Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f211a7f7f | |||
| 7b47fc814e | |||
| 18e2fb8c3e | |||
| c6a7312b57 |
@@ -135,7 +135,7 @@ i18n-extract-enterprise:
|
||||
@echo "Skipping i18n extract for Enterprise: not enabled"
|
||||
else
|
||||
i18n-extract-enterprise:
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary
|
||||
endif
|
||||
|
||||
@@ -227,10 +227,6 @@ fix-cue:
|
||||
gen-jsonnet:
|
||||
go generate ./devenv/jsonnet
|
||||
|
||||
.PHONY: gen-themes
|
||||
gen-themes:
|
||||
go generate ./pkg/services/preference
|
||||
|
||||
.PHONY: update-workspace
|
||||
update-workspace: gen-go
|
||||
@echo "updating workspace"
|
||||
@@ -248,7 +244,6 @@ build-go-fast: ## Build all Go binaries without updating workspace.
|
||||
.PHONY: build-backend
|
||||
build-backend: ## Build Grafana backend.
|
||||
@echo "build backend"
|
||||
$(MAKE) gen-themes
|
||||
$(GO) run build.go $(GO_BUILD_FLAGS) build-backend
|
||||
|
||||
.PHONY: build-air
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
|
||||
github.com/grafana/grafana-app-sdk v0.48.7
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7
|
||||
|
||||
@@ -243,8 +243,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
|
||||
|
||||
+5
-2
@@ -46,6 +46,8 @@ replace github.com/grafana/grafana/apps/annotation => ../annotation
|
||||
|
||||
replace github.com/grafana/grafana/apps/collections => ../collections
|
||||
|
||||
replace github.com/grafana/grafana/pkg/semconv => ../../pkg/semconv
|
||||
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
|
||||
require (
|
||||
@@ -94,7 +96,7 @@ require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect
|
||||
github.com/Yiling-J/theine-go v0.6.2 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
|
||||
@@ -235,8 +237,9 @@ require (
|
||||
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0-20260113153209-1d3f09d5193a // indirect
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
|
||||
+6
-6
@@ -122,8 +122,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
|
||||
@@ -186,8 +186,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
|
||||
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
|
||||
@@ -871,10 +871,10 @@ github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6B
|
||||
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
|
||||
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg=
|
||||
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0-20260113153209-1d3f09d5193a h1:UmIA+Amg0Kgm56vSyqCUQQ5MBD4svQ+smCzIbAXYzyo=
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0-20260113153209-1d3f09d5193a/go.mod h1:b9WxBFbMdf6pDxy90WRFHMyBl1/o8xY86SqnLLLN/yQ=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
|
||||
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
|
||||
|
||||
@@ -17,4 +17,8 @@ teambindingv0alpha1: teambindingKind & {
|
||||
schema: {
|
||||
spec: v0alpha1.TeamBindingSpec
|
||||
}
|
||||
SelectableFields: [
|
||||
"spec.teamRef.name",
|
||||
"spec.subject.name",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -334,6 +334,22 @@ func AddAuthNKnownTypes(scheme *runtime.Scheme) error {
|
||||
&metav1.PartialObjectMetadata{},
|
||||
&metav1.PartialObjectMetadataList{},
|
||||
)
|
||||
|
||||
// Enable field selectors for TeamBinding
|
||||
err := scheme.AddFieldLabelConversionFunc(
|
||||
TeamBindingResourceInfo.GroupVersionKind(),
|
||||
func(label, value string) (string, string, error) {
|
||||
switch label {
|
||||
case "metadata.name", "metadata.namespace", "spec.teamRef.name", "spec.subject.name":
|
||||
return label, value, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("field label not supported for TeamBinding: %s", label)
|
||||
}
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+25
-1
@@ -5,13 +5,37 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaTeamBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewTeamBinding(), &TeamBindingList{}, resource.WithKind("TeamBinding"),
|
||||
resource.WithPlural("teambindings"), resource.WithScope(resource.NamespacedScope))
|
||||
resource.WithPlural("teambindings"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{
|
||||
FieldSelector: "spec.teamRef.name",
|
||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||
cast, ok := o.(*TeamBinding)
|
||||
if !ok {
|
||||
return "", errors.New("provided object must be of type *TeamBinding")
|
||||
}
|
||||
|
||||
return cast.Spec.TeamRef.Name, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
FieldSelector: "spec.subject.name",
|
||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||
cast, ok := o.(*TeamBinding)
|
||||
if !ok {
|
||||
return "", errors.New("provided object must be of type *TeamBinding")
|
||||
}
|
||||
|
||||
return cast.Spec.Subject.Name, nil
|
||||
},
|
||||
},
|
||||
}))
|
||||
kindTeamBinding = resource.Kind{
|
||||
Schema: schemaTeamBinding,
|
||||
Codecs: map[resource.KindEncoding]resource.Codec{
|
||||
|
||||
Generated
+4
@@ -147,6 +147,10 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "TeamBindings",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
SelectableFields: []string{
|
||||
"spec.teamRef.name",
|
||||
"spec.subject.name",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ require (
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
|
||||
+2
-2
@@ -215,8 +215,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
|
||||
+11
-12
@@ -66,18 +66,17 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has
|
||||
|
||||
The following list contains app plugins that have fine-grained RBAC support.
|
||||
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| [Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/) | `grafana-synthetic-monitoring-app` | [Configure RBAC for Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/user-and-team-management/) |
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
|
||||
### Revoke fine-grained access from app plugins
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
import { setScopes } from '../utils/scope-helpers';
|
||||
|
||||
import {
|
||||
getAdHocFilterOptionValues,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
} from './cuj-selectors';
|
||||
import { prepareAPIMocks } from './utils';
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
|
||||
|
||||
test.use({
|
||||
@@ -36,11 +34,6 @@ test.describe(
|
||||
const adHocFilterPills = getAdHocFilterPills(page);
|
||||
const scopesSelectorInput = getScopesSelectorInput(page);
|
||||
|
||||
// Set up routes before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, testScopes());
|
||||
}
|
||||
|
||||
await test.step('1.Apply filtering to a whole dashboard', async () => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
|
||||
@@ -66,17 +66,6 @@ export function getScopesDashboards(page: Page) {
|
||||
return page.locator('[data-testid^="scopes-dashboards-"][role="treeitem"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the first available dashboard in the scopes dashboard list.
|
||||
*/
|
||||
export async function clickFirstScopesDashboard(page: Page) {
|
||||
const dashboards = getScopesDashboards(page);
|
||||
// Wait for at least one dashboard to be visible
|
||||
await expect(dashboards.first()).toBeVisible({ timeout: 10000 });
|
||||
// Click - Playwright will automatically wait for the element to be actionable
|
||||
await dashboards.first().click();
|
||||
}
|
||||
|
||||
export function getScopesDashboardsSearchInput(page: Page) {
|
||||
return page.getByTestId('scopes-dashboards-search');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
import { setScopes } from '../utils/scope-helpers';
|
||||
|
||||
import {
|
||||
clickFirstScopesDashboard,
|
||||
getAdHocFilterPills,
|
||||
getGroupByInput,
|
||||
getGroupByValues,
|
||||
@@ -23,7 +21,6 @@ test.use({
|
||||
},
|
||||
});
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
|
||||
const DASHBOARD_UNDER_TEST_2 = 'cuj-dashboard-2';
|
||||
const NAVIGATE_TO = 'cuj-dashboard-3';
|
||||
@@ -41,11 +38,6 @@ test.describe(
|
||||
const adhocFilterPills = getAdHocFilterPills(page);
|
||||
const groupByValues = getGroupByValues(page);
|
||||
|
||||
// Set up routes before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, testScopes());
|
||||
}
|
||||
|
||||
await test.step('1.Search dashboard', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
@@ -82,7 +74,7 @@ test.describe(
|
||||
|
||||
await expect(markdownContent).toContainText(`now-12h`);
|
||||
|
||||
await clickFirstScopesDashboard(page);
|
||||
await scopesDashboards.first().click();
|
||||
await page.waitForURL('**/d/**');
|
||||
|
||||
await expect(markdownContent).toBeVisible();
|
||||
@@ -125,10 +117,10 @@ test.describe(
|
||||
await groupByVariable.press('Enter');
|
||||
await groupByVariable.press('Escape');
|
||||
|
||||
const { getRequests, waitForExpectedRequests } = await trackDashboardReloadRequests(page);
|
||||
await expect(scopesDashboards.first()).toBeVisible();
|
||||
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
const { getRequests, waitForExpectedRequests } = await trackDashboardReloadRequests(page);
|
||||
await scopesDashboards.first().click();
|
||||
await waitForExpectedRequests();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
@@ -166,7 +158,8 @@ test.describe(
|
||||
const oldFilters = `GroupByVar: ${selectedValues}\n\nAdHocVar: ${processedPills}`;
|
||||
await expect(markdownContent).toContainText(oldFilters);
|
||||
|
||||
await clickFirstScopesDashboard(page);
|
||||
await expect(scopesDashboards.first()).toBeVisible();
|
||||
await scopesDashboards.first().click();
|
||||
await page.waitForURL('**/d/**');
|
||||
|
||||
const newPillCount = await adhocFilterPills.count();
|
||||
|
||||
@@ -165,8 +165,9 @@ test.describe(
|
||||
|
||||
await refreshBtn.click();
|
||||
|
||||
// Wait for the panel content to change (not just for network to complete)
|
||||
await expect(panelContent).not.toHaveText(panelContents!, { timeout: 10000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
expect(await panelContent.textContent()).not.toBe(panelContents);
|
||||
});
|
||||
|
||||
await test.step('6.Turn off refresh', async () => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
openScopesSelector,
|
||||
searchScopes,
|
||||
selectScope,
|
||||
setupScopeRoutes,
|
||||
} from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
@@ -37,37 +36,32 @@ test.describe(
|
||||
const scopesSelector = getScopesSelectorInput(page);
|
||||
const recentScopesSelector = getRecentScopesSelector(page);
|
||||
const scopeTreeCheckboxes = getScopeTreeCheckboxes(page);
|
||||
const scopes = testScopes();
|
||||
|
||||
// Set up routes once before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
}
|
||||
|
||||
await test.step('1.View and select any scope', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const selectedScopes = [secondLevelScopes[0]];
|
||||
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
|
||||
|
||||
scopeName = await getScopeLeafName(page, 0);
|
||||
let scopeTitle = await getScopeLeafTitle(page, 0);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitle);
|
||||
});
|
||||
@@ -76,27 +70,28 @@ test.describe(
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const scopeTitles: string[] = [];
|
||||
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]];
|
||||
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]]; //used only in mocked scopes version
|
||||
for (let i = 0; i < selectedScopes.length; i++) {
|
||||
scopeName = await getScopeLeafName(page, i);
|
||||
scopeTitles.push(await getScopeLeafTitle(page, i));
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]); //used only in mocked scopes version
|
||||
}
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
|
||||
|
||||
await expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitles.join(' + '));
|
||||
});
|
||||
@@ -107,7 +102,8 @@ test.describe(
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
|
||||
await recentScopesSelector.click();
|
||||
|
||||
@@ -125,25 +121,26 @@ test.describe(
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const firstLevelScopes = scopes[2].children!;
|
||||
const firstLevelScopes = scopes[2].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const selectedScopes = [secondLevelScopes[0]];
|
||||
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
|
||||
|
||||
scopeName = await getScopeLeafName(page, 0);
|
||||
let scopeTitle = await getScopeLeafTitle(page, 0);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : []);
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : []); //used only in mocked scopes version
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', new RegExp(`^${scopeTitle}`));
|
||||
});
|
||||
@@ -151,16 +148,17 @@ test.describe(
|
||||
await test.step('5.View pre-completed production entity values as I type', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const scopeSearchOne = await getScopeLeafTitle(page, 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { applyScopes, openScopesSelector, selectScope, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { applyScopes, openScopesSelector, selectScope } from '../utils/scope-helpers';
|
||||
import { testScopesWithRedirect } from '../utils/scopes';
|
||||
|
||||
test.use({
|
||||
@@ -16,13 +16,8 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should redirect to custom URL when scope has redirectUrl', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
// Set up routes BEFORE navigation to ensure all requests are mocked
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await test.step('Navigate to dashboard and open scopes selector', async () => {
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Open scopes selector', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
});
|
||||
|
||||
@@ -45,12 +40,8 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should prioritize redirectUrl over scope navigation fallback', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await test.step('Navigate to dashboard and open scopes selector', async () => {
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Open scopes selector', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
});
|
||||
|
||||
@@ -77,12 +68,8 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
}) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await test.step('Navigate to dashboard and select scope', async () => {
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Select and apply scope', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
|
||||
await applyScopes(page, [scopes[1]]);
|
||||
@@ -125,12 +112,8 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
}) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await test.step('Navigate to dashboard and select scope', async () => {
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Select and apply scope', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
|
||||
await applyScopes(page, [scopes[1]]);
|
||||
@@ -168,13 +151,9 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should not redirect to redirectPath when on active scope navigation', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Set up scope navigation to dashboard-1', async () => {
|
||||
// First, apply a scope that creates scope navigation to dashboard-1 (without redirectPath)
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-setup', scopes[2]);
|
||||
await applyScopes(page, [scopes[2]]);
|
||||
|
||||
@@ -6,150 +6,7 @@ import { Resource } from '../../public/app/features/apiserver/types';
|
||||
|
||||
import { testScopes } from './scopes';
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
|
||||
/**
|
||||
* Sets up all scope-related API routes before navigation.
|
||||
* This ensures that ALL scope API requests (including those made during initial page load)
|
||||
* are intercepted by the mocks, preventing RTK Query from caching real API responses.
|
||||
*
|
||||
* Call this BEFORE navigating to a page (e.g., before gotoDashboardPage).
|
||||
*/
|
||||
export async function setupScopeRoutes(page: Page, scopes: TestScope[]): Promise<void> {
|
||||
// Route for scope node children (tree structure)
|
||||
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_node_children*`, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const parentParam = url.searchParams.get('parent');
|
||||
const queryParam = url.searchParams.get('query');
|
||||
|
||||
// Find the appropriate scopes based on parent
|
||||
let scopesToReturn = scopes;
|
||||
if (parentParam) {
|
||||
// Find nested scopes based on parent name
|
||||
const findChildren = (items: TestScope[]): TestScope[] => {
|
||||
for (const item of items) {
|
||||
if (item.name === parentParam && item.children) {
|
||||
return item.children;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findChildren(item.children);
|
||||
if (found.length > 0) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
scopesToReturn = findChildren(scopes);
|
||||
if (scopesToReturn.length === 0) {
|
||||
scopesToReturn = scopes; // Fallback to root scopes
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search query if provided
|
||||
if (queryParam) {
|
||||
const query = queryParam.toLowerCase();
|
||||
const filterByQuery = (items: TestScope[]): TestScope[] => {
|
||||
const results: TestScope[] = [];
|
||||
for (const item of items) {
|
||||
// Exact match on name or title containing the query
|
||||
if (item.name.toLowerCase() === query || item.title.toLowerCase() === query) {
|
||||
results.push(item);
|
||||
} else if (item.name.toLowerCase().includes(query) || item.title.toLowerCase().includes(query)) {
|
||||
results.push(item);
|
||||
}
|
||||
// Also search in children
|
||||
if (item.children) {
|
||||
results.push(...filterByQuery(item.children));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
scopesToReturn = filterByQuery(scopesToReturn);
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
kind: 'FindScopeNodeChildrenResults',
|
||||
metadata: {},
|
||||
items: scopesToReturn.map((scope) => ({
|
||||
kind: 'ScopeNode',
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
metadata: {
|
||||
name: scope.name,
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
title: scope.title,
|
||||
description: scope.title,
|
||||
disableMultiSelect: scope.disableMultiSelect ?? false,
|
||||
nodeType: scope.children ? 'container' : 'leaf',
|
||||
...(parentParam && { parentName: parentParam }),
|
||||
...((scope.addLinks || scope.children) && {
|
||||
linkType: 'scope',
|
||||
linkId: `scope-${scope.name}`,
|
||||
}),
|
||||
...(scope.redirectPath && { redirectPath: scope.redirectPath }),
|
||||
},
|
||||
})),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Route for individual scope fetching
|
||||
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/*`, async (route) => {
|
||||
const url = route.request().url();
|
||||
const scopeName = url.split('/scopes/')[1]?.split('?')[0];
|
||||
|
||||
// Find the scope in the test data
|
||||
const findScope = (items: TestScope[]): TestScope | undefined => {
|
||||
for (const item of items) {
|
||||
if (`scope-${item.name}` === scopeName) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findScope(item.children);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const scope = findScope(scopes);
|
||||
|
||||
if (scope) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
kind: 'Scope',
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
metadata: {
|
||||
name: `scope-${scope.name}`,
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
title: scope.title,
|
||||
description: '',
|
||||
filters: scope.filters,
|
||||
category: scope.category,
|
||||
type: scope.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({ status: 404 });
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Dashboard bindings and navigations routes are set up dynamically in applyScopes()
|
||||
// with scope-specific URL patterns to avoid cache issues. They are not set up here.
|
||||
}
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CALLS_CONFIG_PATH);
|
||||
|
||||
export type TestScope = {
|
||||
name: string;
|
||||
@@ -167,9 +24,6 @@ export type TestScope = {
|
||||
|
||||
type ScopeDashboardBinding = Resource<ScopeDashboardBindingSpec, ScopeDashboardBindingStatus, 'ScopeDashboardBinding'>;
|
||||
|
||||
/**
|
||||
* Sets up a route for scope node children requests and waits for the response.
|
||||
*/
|
||||
export async function scopeNodeChildrenRequest(
|
||||
page: Page,
|
||||
scopes: TestScope[],
|
||||
@@ -214,13 +68,10 @@ export async function scopeNodeChildrenRequest(
|
||||
return page.waitForResponse((response) => response.url().includes(`/find/scope_node_children`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the scopes selector dropdown and waits for the tree to load.
|
||||
*/
|
||||
export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId('scopes-selector-input').click();
|
||||
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
if (!scopes) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -231,13 +82,10 @@ export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a scope tree node and waits for children to load.
|
||||
*/
|
||||
export async function expandScopesSelection(page: Page, parentScope: string, scopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId(`scopes-tree-${parentScope}-expand`).click();
|
||||
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
if (!scopes) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -248,9 +96,6 @@ export async function expandScopesSelection(page: Page, parentScope: string, sco
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a route for individual scope requests and waits for the response.
|
||||
*/
|
||||
export async function scopeSelectRequest(page: Page, selectedScope: TestScope): Promise<Response> {
|
||||
await page.route(
|
||||
`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/scope-${selectedScope.name}`,
|
||||
@@ -280,9 +125,6 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope):
|
||||
return page.waitForResponse((response) => response.url().includes(`/scopes/scope-${selectedScope.name}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a scope in the tree.
|
||||
*/
|
||||
export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) {
|
||||
const click = async () => {
|
||||
const element = page.locator(
|
||||
@@ -292,7 +134,7 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
|
||||
await element.click({ force: true });
|
||||
};
|
||||
|
||||
if (!selectedScope || USE_LIVE_DATA) {
|
||||
if (!selectedScope) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -303,22 +145,14 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected scopes and waits for the selector to close and page to settle.
|
||||
* Sets up routes dynamically with scope-specific URL patterns to avoid cache issues.
|
||||
*/
|
||||
export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
const click = async () => {
|
||||
await page.getByTestId('scopes-selector-apply').scrollIntoViewIfNeeded();
|
||||
await page.getByTestId('scopes-selector-apply').click({ force: true });
|
||||
};
|
||||
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
if (!scopes) {
|
||||
await click();
|
||||
// Wait for the apply button to disappear (selector closed)
|
||||
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
|
||||
// Wait for any resulting API calls (dashboard bindings, etc.) to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,7 +166,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
|
||||
const groups: string[] = ['Most relevant', 'Dashboards', 'Something else', ''];
|
||||
|
||||
// Mock scope_dashboard_bindings endpoint with scope-specific URL pattern
|
||||
// Mock scope_dashboard_bindings endpoint
|
||||
await page.route(dashboardBindingsUrl, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -386,7 +220,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
});
|
||||
});
|
||||
|
||||
// Mock scope_navigations endpoint with scope-specific URL pattern
|
||||
// Mock scope_navigations endpoint
|
||||
await page.route(scopeNavigationsUrl, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -432,23 +266,21 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
(response) =>
|
||||
response.url().includes(`/find/scope_dashboard_bindings`) || response.url().includes(`/find/scope_navigations`)
|
||||
);
|
||||
const scopeRequestPromises: Array<Promise<Response>> = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
scopeRequestPromises.push(scopeSelectRequest(page, scope));
|
||||
}
|
||||
|
||||
await click();
|
||||
await responsePromise;
|
||||
// Wait for the apply button to disappear (selector closed)
|
||||
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
|
||||
// Wait for any resulting API calls to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
await Promise.all(scopeRequestPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for scopes in the tree and waits for results.
|
||||
* Sets up a route dynamically with filtered results to return only matching scopes.
|
||||
*/
|
||||
export async function searchScopes(page: Page, value: string, resultScopes?: TestScope[]) {
|
||||
export async function searchScopes(page: Page, value: string, resultScopes: TestScope[]) {
|
||||
const click = async () => await page.getByTestId('scopes-tree-search').fill(value);
|
||||
|
||||
if (!resultScopes || USE_LIVE_DATA) {
|
||||
if (!resultScopes) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = false
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -32,14 +32,14 @@ require (
|
||||
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect; @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect; @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||
@@ -82,14 +82,14 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group
|
||||
github.com/golang/snappy v1.0.0 // @grafana/alerting-backend
|
||||
github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group
|
||||
github.com/google/go-github/v70 v70.0.0 // @grafana/grafana-git-ui-sync-team
|
||||
github.com/google/go-github/v70 v70.0.0 // indirect; @grafana/grafana-git-ui-sync-team
|
||||
github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent
|
||||
github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group
|
||||
github.com/google/wire v0.7.0 // @grafana/grafana-backend-group
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
@@ -113,7 +113,6 @@ require (
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // @grafana/grafana-backend-group
|
||||
@@ -261,13 +260,12 @@ require (
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0 // @grafana/plugins-platform-backend
|
||||
|
||||
// This needs to be here for other projects that import grafana/grafana
|
||||
// For local development grafana/grafana will always use the local files
|
||||
// Check go.work file for details
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // @grafana/grafana-app-platform-squad
|
||||
)
|
||||
|
||||
// Replace the workspace versions
|
||||
@@ -296,8 +294,6 @@ replace (
|
||||
github.com/grafana/grafana/pkg/aggregator => ./pkg/aggregator
|
||||
github.com/grafana/grafana/pkg/apimachinery => ./pkg/apimachinery
|
||||
github.com/grafana/grafana/pkg/apiserver => ./pkg/apiserver
|
||||
github.com/grafana/grafana/pkg/plugins => ./pkg/plugins
|
||||
github.com/grafana/grafana/pkg/semconv => ./pkg/semconv
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -656,12 +652,11 @@ require (
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
|
||||
github.com/Machiel/slugify v1.0.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
@@ -681,8 +676,6 @@ require (
|
||||
github.com/google/gnostic v0.7.1 // indirect
|
||||
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 // indirect
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
@@ -704,7 +697,7 @@ require (
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
|
||||
|
||||
// Use our fork of the upstream Alertmanager.
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
|
||||
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
|
||||
|
||||
@@ -680,7 +680,6 @@ github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7Og
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
@@ -738,8 +737,6 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U=
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2/go.mod h1:+vWzISN6S9ssgurhUNmm6AlXL9XLah3TdWJktquKTR8=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
|
||||
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
@@ -762,8 +759,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
|
||||
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@@ -1031,8 +1026,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -1627,8 +1620,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
@@ -1671,6 +1664,8 @@ github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDP
|
||||
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
|
||||
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
|
||||
@@ -1681,8 +1676,8 @@ github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ
|
||||
github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f h1:fTlIj5n4x5dU63XHItug7GLjtnaeJdPqBlqg4zlABq0=
|
||||
@@ -1758,8 +1753,6 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
@@ -1884,10 +1877,6 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
|
||||
@@ -38,6 +38,6 @@ use (
|
||||
./pkg/semconv
|
||||
)
|
||||
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
|
||||
|
||||
+1
-5
@@ -280,6 +280,7 @@ github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw
|
||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
|
||||
@@ -905,8 +906,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
|
||||
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
|
||||
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3 h1:KVncUdAc5YwY/OQmw6HgzJmbRKn6IwrhvtcBAd1yDHo=
|
||||
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3/go.mod h1:Oy4MthJqfErlieO14ryZXdukDrUACy8Lg56P3zP7S1k=
|
||||
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
|
||||
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
@@ -997,8 +996,6 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
@@ -1914,7 +1911,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
|
||||
|
||||
+4
-2
@@ -62,7 +62,8 @@
|
||||
"stats": "webpack --mode production --config scripts/webpack/webpack.prod.js --profile --json > compilation-stats.json",
|
||||
"storybook": "yarn workspace @grafana/ui storybook --ci",
|
||||
"storybook:build": "yarn workspace @grafana/ui storybook:build",
|
||||
"themes-generate": "yarn workspace @grafana/data themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json",
|
||||
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'",
|
||||
"typecheck": "tsc --noEmit && yarn run packages:typecheck",
|
||||
"plugins:build-bundled": "echo 'bundled plugins are no longer supported'",
|
||||
@@ -253,6 +254,7 @@
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-json-schema": "^0.65.1",
|
||||
"webpack": "5.101.0",
|
||||
"webpack-assets-manifest": "^5.1.0",
|
||||
"webpack-cli": "6.0.1",
|
||||
@@ -263,7 +265,7 @@
|
||||
"webpackbar": "^7.0.0",
|
||||
"yaml": "^2.0.0",
|
||||
"yargs": "^18.0.0",
|
||||
"zod": "^4.3.0"
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bsull/augurs": "^0.10.0",
|
||||
|
||||
@@ -34,8 +34,6 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
|
||||
getBackendSrv().fetch({
|
||||
...requestOptions,
|
||||
url: baseURL + requestOptions.url,
|
||||
// Default to GET so backend_srv correctly skips success alerts for queries
|
||||
method: requestOptions.method ?? 'GET',
|
||||
showErrorAlert: requestOptions.showErrorAlert ?? false,
|
||||
data: requestOptions.body,
|
||||
headers,
|
||||
|
||||
@@ -47,12 +47,11 @@
|
||||
"LICENSE_APACHE2"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn themes-schema && tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
@@ -82,12 +81,10 @@
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.8.1",
|
||||
"uplot": "1.6.32",
|
||||
"xss": "^1.0.14",
|
||||
"zod": "^4.3.0"
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/scenes": "6.38.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@types/history": "4.7.11",
|
||||
@@ -104,7 +101,6 @@
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json from '@rollup/plugin-json';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
|
||||
@@ -9,13 +8,13 @@ const pkg = rq('./package.json');
|
||||
export default [
|
||||
{
|
||||
input: entryPoint,
|
||||
plugins: [...plugins, json()],
|
||||
plugins,
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins: [...plugins, json()],
|
||||
plugins,
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
|
||||
@@ -106,4 +106,3 @@ export { findNumericFieldMinMax } from '../field/fieldOverrides';
|
||||
export { type PanelOptionsSupplier } from '../panel/PanelPlugin';
|
||||
export { sanitize, sanitizeUrl } from '../text/sanitize';
|
||||
export { type NestedValueAccess, type NestedPanelOptions, isNestedPanelOptions } from '../utils/OptionsUIBuilders';
|
||||
export { NewThemeOptionsSchema } from '../themes/createTheme';
|
||||
|
||||
@@ -1,103 +1,83 @@
|
||||
import { merge } from 'lodash';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { alpha, darken, emphasize, getContrastRatio, lighten } from './colorManipulator';
|
||||
import { palette } from './palette';
|
||||
import { DeepRequired, ThemeRichColor, ThemeRichColorInputSchema } from './types';
|
||||
import { DeepPartial, ThemeRichColor } from './types';
|
||||
|
||||
const ThemeColorsModeSchema = z.enum(['light', 'dark']);
|
||||
/** @internal */
|
||||
export type ThemeColorsMode = z.infer<typeof ThemeColorsModeSchema>;
|
||||
export type ThemeColorsMode = 'light' | 'dark';
|
||||
|
||||
const createThemeColorsBaseSchema = <TColor>(color: TColor) =>
|
||||
z
|
||||
.object({
|
||||
mode: ThemeColorsModeSchema,
|
||||
|
||||
primary: color,
|
||||
secondary: color,
|
||||
info: color,
|
||||
error: color,
|
||||
success: color,
|
||||
warning: color,
|
||||
|
||||
text: z.object({
|
||||
primary: z.string().optional(),
|
||||
secondary: z.string().optional(),
|
||||
disabled: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
/** Used for auto white or dark text on colored backgrounds */
|
||||
maxContrast: z.string().optional(),
|
||||
}),
|
||||
|
||||
background: z.object({
|
||||
/** Dashboard and body background */
|
||||
canvas: z.string().optional(),
|
||||
/** Primary content pane background (panels etc) */
|
||||
primary: z.string().optional(),
|
||||
/** Cards and elements that need to stand out on the primary background */
|
||||
secondary: z.string().optional(),
|
||||
/**
|
||||
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
|
||||
* themes it has a brighter shade to help give it contrast against the primary background.
|
||||
**/
|
||||
elevated: z.string().optional(),
|
||||
}),
|
||||
|
||||
border: z.object({
|
||||
weak: z.string().optional(),
|
||||
medium: z.string().optional(),
|
||||
strong: z.string().optional(),
|
||||
}),
|
||||
|
||||
gradients: z.object({
|
||||
brandVertical: z.string().optional(),
|
||||
brandHorizontal: z.string().optional(),
|
||||
}),
|
||||
|
||||
action: z.object({
|
||||
/** Used for selected menu item / select option */
|
||||
selected: z.string().optional(),
|
||||
/**
|
||||
* @alpha (Do not use from plugins)
|
||||
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
|
||||
**/
|
||||
selectedBorder: z.string().optional(),
|
||||
/** Used for hovered menu item / select option */
|
||||
hover: z.string().optional(),
|
||||
/** Used for button/colored background hover opacity */
|
||||
hoverOpacity: z.number().optional(),
|
||||
/** Used focused menu item / select option */
|
||||
focus: z.string().optional(),
|
||||
/** Used for disabled buttons and inputs */
|
||||
disabledBackground: z.string().optional(),
|
||||
/** Disabled text */
|
||||
disabledText: z.string().optional(),
|
||||
/** Disablerd opacity */
|
||||
disabledOpacity: z.number().optional(),
|
||||
}),
|
||||
|
||||
hoverFactor: z.number(),
|
||||
contrastThreshold: z.number(),
|
||||
tonalOffset: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
// Need to override the zod type to include the generic properly
|
||||
/** @internal */
|
||||
export type ThemeColorsBase<TColor> = DeepRequired<
|
||||
Omit<
|
||||
z.infer<ReturnType<typeof createThemeColorsBaseSchema>>,
|
||||
'primary' | 'secondary' | 'info' | 'error' | 'success' | 'warning'
|
||||
>
|
||||
> & {
|
||||
export interface ThemeColorsBase<TColor> {
|
||||
mode: ThemeColorsMode;
|
||||
|
||||
primary: TColor;
|
||||
secondary: TColor;
|
||||
info: TColor;
|
||||
error: TColor;
|
||||
success: TColor;
|
||||
warning: TColor;
|
||||
};
|
||||
|
||||
text: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
disabled: string;
|
||||
link: string;
|
||||
/** Used for auto white or dark text on colored backgrounds */
|
||||
maxContrast: string;
|
||||
};
|
||||
|
||||
background: {
|
||||
/** Dashboard and body background */
|
||||
canvas: string;
|
||||
/** Primary content pane background (panels etc) */
|
||||
primary: string;
|
||||
/** Cards and elements that need to stand out on the primary background */
|
||||
secondary: string;
|
||||
/**
|
||||
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
|
||||
* themes it has a brighter shade to help give it contrast against the primary background.
|
||||
**/
|
||||
elevated: string;
|
||||
};
|
||||
|
||||
border: {
|
||||
weak: string;
|
||||
medium: string;
|
||||
strong: string;
|
||||
};
|
||||
|
||||
gradients: {
|
||||
brandVertical: string;
|
||||
brandHorizontal: string;
|
||||
};
|
||||
|
||||
action: {
|
||||
/** Used for selected menu item / select option */
|
||||
selected: string;
|
||||
/**
|
||||
* @alpha (Do not use from plugins)
|
||||
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
|
||||
**/
|
||||
selectedBorder: string;
|
||||
/** Used for hovered menu item / select option */
|
||||
hover: string;
|
||||
/** Used for button/colored background hover opacity */
|
||||
hoverOpacity: number;
|
||||
/** Used focused menu item / select option */
|
||||
focus: string;
|
||||
/** Used for disabled buttons and inputs */
|
||||
disabledBackground: string;
|
||||
/** Disabled text */
|
||||
disabledText: string;
|
||||
/** Disablerd opacity */
|
||||
disabledOpacity: number;
|
||||
};
|
||||
|
||||
hoverFactor: number;
|
||||
contrastThreshold: number;
|
||||
tonalOffset: number;
|
||||
}
|
||||
|
||||
export interface ThemeHoverStrengh {}
|
||||
|
||||
@@ -109,10 +89,8 @@ export interface ThemeColors extends ThemeColorsBase<ThemeRichColor> {
|
||||
emphasize(color: string, amount?: number): string;
|
||||
}
|
||||
|
||||
export const ThemeColorsInputSchema = createThemeColorsBaseSchema(ThemeRichColorInputSchema);
|
||||
|
||||
/** @internal */
|
||||
export type ThemeColorsInput = z.infer<typeof ThemeColorsInputSchema>;
|
||||
export type ThemeColorsInput = DeepPartial<ThemeColorsBase<ThemeRichColor>>;
|
||||
|
||||
class DarkColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
|
||||
mode: ThemeColorsMode = 'dark';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @beta */
|
||||
export interface ThemeShape {
|
||||
/**
|
||||
@@ -36,12 +34,9 @@ export interface Radii {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const ThemeShapeInputSchema = z.object({
|
||||
borderRadius: z.int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export type ThemeShapeInput = z.infer<typeof ThemeShapeInputSchema>;
|
||||
export interface ThemeShapeInput {
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
export function createShape(options: ThemeShapeInput): ThemeShape {
|
||||
const baseBorderRadius = options.borderRadius ?? 6;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// Code based on Material UI
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2014 Call-Em-All
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @internal */
|
||||
export const ThemeSpacingOptionsSchema = z.object({
|
||||
gridSize: z.int().positive().optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingOptions = z.infer<typeof ThemeSpacingOptionsSchema>;
|
||||
export type ThemeSpacingOptions = {
|
||||
gridSize?: number;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingArgument = number | string;
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { createBreakpoints } from './breakpoints';
|
||||
import { createColors, ThemeColorsInputSchema } from './createColors';
|
||||
import { createColors, ThemeColorsInput } from './createColors';
|
||||
import { createComponents } from './createComponents';
|
||||
import { createShadows } from './createShadows';
|
||||
import { createShape, ThemeShapeInputSchema } from './createShape';
|
||||
import { createSpacing, ThemeSpacingOptionsSchema } from './createSpacing';
|
||||
import { createShape, ThemeShapeInput } from './createShape';
|
||||
import { createSpacing, ThemeSpacingOptions } from './createSpacing';
|
||||
import { createTransitions } from './createTransitions';
|
||||
import { createTypography, ThemeTypographyInputSchema } from './createTypography';
|
||||
import { createTypography, ThemeTypographyInput } from './createTypography';
|
||||
import { createV1Theme } from './createV1Theme';
|
||||
import { createVisualizationColors, ThemeVisualizationColorsInputSchema } from './createVisualizationColors';
|
||||
import { createVisualizationColors, ThemeVisualizationColorsInput } from './createVisualizationColors';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
import { zIndex } from './zIndex';
|
||||
|
||||
export const NewThemeOptionsSchema = z.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
colors: ThemeColorsInputSchema.optional(),
|
||||
spacing: ThemeSpacingOptionsSchema.optional(),
|
||||
shape: ThemeShapeInputSchema.optional(),
|
||||
typography: ThemeTypographyInputSchema.optional(),
|
||||
visualization: ThemeVisualizationColorsInputSchema.optional(),
|
||||
});
|
||||
/** @internal */
|
||||
export interface NewThemeOptions {
|
||||
name?: string;
|
||||
colors?: ThemeColorsInput;
|
||||
spacing?: ThemeSpacingOptions;
|
||||
shape?: ThemeShapeInput;
|
||||
typography?: ThemeTypographyInput;
|
||||
visualization?: ThemeVisualizationColorsInput;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type NewThemeOptions = z.infer<typeof NewThemeOptionsSchema>;
|
||||
|
||||
/** @internal */
|
||||
export function createTheme(
|
||||
options: Omit<NewThemeOptions, 'id' | 'name'> & {
|
||||
name?: NewThemeOptions['name'];
|
||||
} = {}
|
||||
): GrafanaTheme2 {
|
||||
export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 {
|
||||
const {
|
||||
name,
|
||||
colors: colorsInput = {},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Code based on Material UI
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2014 Call-Em-All
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ThemeColors } from './createColors';
|
||||
|
||||
@@ -41,20 +40,18 @@ export interface ThemeTypographyVariant {
|
||||
letterSpacing?: string;
|
||||
}
|
||||
|
||||
export const ThemeTypographyInputSchema = z.object({
|
||||
fontFamily: z.string().optional(),
|
||||
fontFamilyMonospace: z.string().optional(),
|
||||
fontSize: z.number().positive().optional(),
|
||||
fontWeightLight: z.number().positive().optional(),
|
||||
fontWeightRegular: z.number().positive().optional(),
|
||||
fontWeightMedium: z.number().positive().optional(),
|
||||
fontWeightBold: z.number().positive().optional(),
|
||||
// what's the font-size on the html element.
|
||||
export interface ThemeTypographyInput {
|
||||
fontFamily?: string;
|
||||
fontFamilyMonospace?: string;
|
||||
fontSize?: number;
|
||||
fontWeightLight?: number;
|
||||
fontWeightRegular?: number;
|
||||
fontWeightMedium?: number;
|
||||
fontWeightBold?: number;
|
||||
// hat's the font-size on the html element.
|
||||
// 16px is the default font-size used by browsers.
|
||||
htmlFontSize: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type ThemeTypographyInput = z.infer<typeof ThemeTypographyInputSchema>;
|
||||
htmlFontSize?: number;
|
||||
}
|
||||
|
||||
const defaultFontFamily = "'Inter', 'Helvetica', 'Arial', sans-serif";
|
||||
const defaultFontFamilyMonospace = "'Roboto Mono', monospace";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FALLBACK_COLOR } from '../types/fieldColor';
|
||||
|
||||
import { ThemeColors } from './createColors';
|
||||
@@ -28,44 +26,29 @@ export interface ThemeVizColor<T extends ThemeVizColorName> {
|
||||
|
||||
type ThemeVizColorName = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple';
|
||||
|
||||
const createShadeSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
|
||||
z.enum([`super-light-${color}`, `light-${color}`, color, `semi-dark-${color}`, `dark-${color}`]);
|
||||
type ThemeVizColorShadeName<T extends ThemeVizColorName> =
|
||||
| `super-light-${T}`
|
||||
| `light-${T}`
|
||||
| T
|
||||
| `semi-dark-${T}`
|
||||
| `dark-${T}`;
|
||||
|
||||
type ThemeVizColorShadeName<T extends ThemeVizColorName> = z.infer<ReturnType<typeof createShadeSchema<T>>>;
|
||||
|
||||
const createHueSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
|
||||
z.object({
|
||||
name: z.literal(color),
|
||||
shades: z.array(
|
||||
z.object({
|
||||
color: z.string(),
|
||||
name: createShadeSchema(color),
|
||||
aliases: z.array(z.string()).optional(),
|
||||
primary: z.boolean().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const ThemeVizHueSchema = z.union([
|
||||
createHueSchema('red'),
|
||||
createHueSchema('orange'),
|
||||
createHueSchema('yellow'),
|
||||
createHueSchema('green'),
|
||||
createHueSchema('blue'),
|
||||
createHueSchema('purple'),
|
||||
]);
|
||||
type ThemeVizHueGeneric<T> = T extends ThemeVizColorName
|
||||
? {
|
||||
name: T;
|
||||
shades: Array<ThemeVizColor<T>>;
|
||||
}
|
||||
: never;
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type ThemeVizHue = z.infer<typeof ThemeVizHueSchema>;
|
||||
export type ThemeVizHue = ThemeVizHueGeneric<ThemeVizColorName>;
|
||||
|
||||
export const ThemeVisualizationColorsInputSchema = z.object({
|
||||
hues: z.array(ThemeVizHueSchema).optional(),
|
||||
palette: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type ThemeVisualizationColorsInput = z.infer<typeof ThemeVisualizationColorsInputSchema>;
|
||||
export type ThemeVisualizationColorsInput = {
|
||||
hues?: ThemeVizHue[];
|
||||
palette?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Registry, RegistryItem } from '../utils/Registry';
|
||||
|
||||
import { createTheme, NewThemeOptionsSchema } from './createTheme';
|
||||
import { createTheme } from './createTheme';
|
||||
import * as extraThemes from './themeDefinitions';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
|
||||
@@ -42,6 +42,9 @@ export function getBuiltInThemes(allowedExtras: string[]) {
|
||||
return sortedThemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is also a backend list at pkg/services/preference/themes.go
|
||||
*/
|
||||
const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
return [
|
||||
{ id: 'system', name: 'System preference', build: getSystemPreferenceTheme },
|
||||
@@ -50,19 +53,13 @@ const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
for (const [name, json] of Object.entries(extraThemes)) {
|
||||
const result = NewThemeOptionsSchema.safeParse(json);
|
||||
if (!result.success) {
|
||||
console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`);
|
||||
} else {
|
||||
const theme = result.data;
|
||||
themeRegistry.register({
|
||||
id: theme.id,
|
||||
name: theme.name,
|
||||
build: () => createTheme(theme),
|
||||
isExtra: true,
|
||||
});
|
||||
}
|
||||
for (const [id, theme] of Object.entries(extraThemes)) {
|
||||
themeRegistry.register({
|
||||
id,
|
||||
name: theme.name ?? '',
|
||||
build: () => createTheme(theme),
|
||||
isExtra: true,
|
||||
});
|
||||
}
|
||||
|
||||
function getSystemPreferenceTheme() {
|
||||
|
||||
@@ -1,608 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"colors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["light", "dark"]
|
||||
},
|
||||
"primary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"secondary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"success": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"warning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxContrast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"background": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canvas": {
|
||||
"type": "string"
|
||||
},
|
||||
"primary": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string"
|
||||
},
|
||||
"elevated": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"border": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"weak": {
|
||||
"type": "string"
|
||||
},
|
||||
"medium": {
|
||||
"type": "string"
|
||||
},
|
||||
"strong": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"gradients": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"brandVertical": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandHorizontal": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"selected": {
|
||||
"type": "string"
|
||||
},
|
||||
"selectedBorder": {
|
||||
"type": "string"
|
||||
},
|
||||
"hover": {
|
||||
"type": "string"
|
||||
},
|
||||
"hoverOpacity": {
|
||||
"type": "number"
|
||||
},
|
||||
"focus": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledBackground": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledText": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledOpacity": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"hoverFactor": {
|
||||
"type": "number"
|
||||
},
|
||||
"contrastThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"tonalOffset": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"spacing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gridSize": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"shape": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"borderRadius": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"typography": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fontFamily": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontFamilyMonospace": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontSize": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightLight": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightRegular": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightMedium": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightBold": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"htmlFontSize": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"visualization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hues": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "red"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-red", "light-red", "red", "semi-dark-red", "dark-red"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "orange"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-orange", "light-orange", "orange", "semi-dark-orange", "dark-orange"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "yellow"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-yellow", "light-yellow", "yellow", "semi-dark-yellow", "dark-yellow"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "green"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-green", "light-green", "green", "semi-dark-green", "dark-green"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "blue"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-blue", "light-blue", "blue", "semi-dark-blue", "dark-blue"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "purple"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-purple", "light-purple", "purple", "semi-dark-purple", "dark-purple"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"palette": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "id"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { NewThemeOptionsSchema } from '../createTheme';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../schema.generated.json'),
|
||||
JSON.stringify(
|
||||
NewThemeOptionsSchema.toJSONSchema({
|
||||
target: 'draft-07',
|
||||
}),
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Aubergine",
|
||||
"id": "aubergine",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#4F2A3D",
|
||||
"medium": "#6A3C4B",
|
||||
"strong": "#8C5A69"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E5D0D6",
|
||||
"secondary": "#D1A8C4",
|
||||
"disabled": "#B7A0A6",
|
||||
"link": "#A56BB6",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#8C5A69"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#6A3C4B",
|
||||
"text": "#D1A8C4",
|
||||
"border": "#8C5A69"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#2E1F2D",
|
||||
"primary": "#3C2136",
|
||||
"secondary": "#4A2D47",
|
||||
"elevated": "#4A2D47"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#6A3C4B",
|
||||
"selected": "#8C5A69",
|
||||
"selectedBorder": "#FFB300",
|
||||
"focus": "#A56BB6",
|
||||
"hoverOpacity": 0.1,
|
||||
"disabledText": "#B7A0A6",
|
||||
"disabledBackground": "#4A2D47",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)"
|
||||
},
|
||||
"contrastThreshold": 4,
|
||||
"hoverFactor": 0.07,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const aubergineTheme: NewThemeOptions = {
|
||||
name: 'Aubergine',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#4F2A3D',
|
||||
medium: '#6A3C4B',
|
||||
strong: '#8C5A69',
|
||||
},
|
||||
text: {
|
||||
primary: '#E5D0D6',
|
||||
secondary: '#D1A8C4',
|
||||
disabled: '#B7A0A6',
|
||||
link: '#A56BB6',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#8C5A69',
|
||||
},
|
||||
secondary: {
|
||||
main: '#6A3C4B',
|
||||
text: '#D1A8C4',
|
||||
border: '#8C5A69',
|
||||
},
|
||||
background: {
|
||||
canvas: '#2E1F2D',
|
||||
primary: '#3C2136',
|
||||
secondary: '#4A2D47',
|
||||
elevated: '#4A2D47',
|
||||
},
|
||||
action: {
|
||||
hover: '#6A3C4B',
|
||||
selected: '#8C5A69',
|
||||
selectedBorder: '#FFB300',
|
||||
focus: '#A56BB6',
|
||||
hoverOpacity: 0.1,
|
||||
disabledText: '#B7A0A6',
|
||||
disabledBackground: '#4A2D47',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)',
|
||||
},
|
||||
contrastThreshold: 4,
|
||||
hoverFactor: 0.07,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default aubergineTheme;
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "Debug",
|
||||
"id": "debug",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"background": {
|
||||
"canvas": "#000033",
|
||||
"primary": "#000044",
|
||||
"secondary": "#000055",
|
||||
"elevated": "#000055"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#bbbb00",
|
||||
"secondary": "#888800",
|
||||
"disabled": "#444400",
|
||||
"link": "#dddd00",
|
||||
"maxContrast": "#ffff00"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#ff000044",
|
||||
"medium": "#ff000088",
|
||||
"strong": "#ff0000ff"
|
||||
},
|
||||
"primary": {
|
||||
"border": "#ff000088",
|
||||
"text": "#cccc00",
|
||||
"contrastText": "#ffff00",
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"secondary": {
|
||||
"border": "#ff000088",
|
||||
"text": "#cccc00",
|
||||
"contrastText": "#ffff00",
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"info": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"warning": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"success": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"error": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#9900dd",
|
||||
"focus": "#6600aa",
|
||||
"selected": "#440088"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"borderRadius": 8
|
||||
},
|
||||
"spacing": {
|
||||
"gridSize": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
/**
|
||||
* a very ugly theme that is useful for debugging and checking if the theme is applied correctly
|
||||
* borders are red,
|
||||
* backgrounds are blue,
|
||||
* text is yellow,
|
||||
* and grafana loves you <3
|
||||
* (also corners are rounded, action states (hover, focus, selected) are purple)
|
||||
*/
|
||||
const debugTheme: NewThemeOptions = {
|
||||
name: 'Debug',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
background: {
|
||||
canvas: '#000033',
|
||||
primary: '#000044',
|
||||
secondary: '#000055',
|
||||
elevated: '#000055',
|
||||
},
|
||||
text: {
|
||||
primary: '#bbbb00',
|
||||
secondary: '#888800',
|
||||
disabled: '#444400',
|
||||
link: '#dddd00',
|
||||
maxContrast: '#ffff00',
|
||||
},
|
||||
border: {
|
||||
weak: '#ff000044',
|
||||
medium: '#ff000088',
|
||||
strong: '#ff0000ff',
|
||||
},
|
||||
primary: {
|
||||
border: '#ff000088',
|
||||
text: '#cccc00',
|
||||
contrastText: '#ffff00',
|
||||
shade: '#9900dd',
|
||||
},
|
||||
secondary: {
|
||||
border: '#ff000088',
|
||||
text: '#cccc00',
|
||||
contrastText: '#ffff00',
|
||||
shade: '#9900dd',
|
||||
},
|
||||
info: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
warning: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
success: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
error: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
action: {
|
||||
hover: '#9900dd',
|
||||
focus: '#6600aa',
|
||||
selected: '#440088',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
spacing: {
|
||||
gridSize: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export default debugTheme;
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"name": "Desert bloom",
|
||||
"id": "desertbloom",
|
||||
"colors": {
|
||||
"mode": "light",
|
||||
"border": {
|
||||
"weak": "rgba(0, 0, 0, 0.12)",
|
||||
"medium": "rgba(0, 0, 0, 0.20)",
|
||||
"strong": "rgba(0, 0, 0, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#333333",
|
||||
"secondary": "#555555",
|
||||
"disabled": "rgba(0, 0, 0, 0.5)",
|
||||
"link": "#1A82E2",
|
||||
"maxContrast": "#000000"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FF6F61",
|
||||
"text": "#FE6F61",
|
||||
"border": "#E55B4D",
|
||||
"name": "primary",
|
||||
"shade": "#E55B4D",
|
||||
"transparent": "#FF6F6126",
|
||||
"contrastText": "#FFFFFF",
|
||||
"borderTransparent": "#FF6F6140"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#FFFFFF",
|
||||
"text": "#695f53",
|
||||
"border": "#d9cec0",
|
||||
"name": "secondary",
|
||||
"shade": "#d9cec0",
|
||||
"transparent": "#FFFFFF26",
|
||||
"contrastText": "#4c4339",
|
||||
"borderTransparent": "#FFFFFF40"
|
||||
},
|
||||
"info": {
|
||||
"main": "#1A82E2"
|
||||
},
|
||||
"success": {
|
||||
"main": "#4CAF50"
|
||||
},
|
||||
"warning": {
|
||||
"main": "#FFC107"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#FFF8F0",
|
||||
"primary": "#FFFFFF",
|
||||
"secondary": "#f9f3e8",
|
||||
"elevated": "#FFFFFF"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(168, 156, 134, 0.12)",
|
||||
"selected": "rgba(168, 156, 134, 0.36)",
|
||||
"selectedBorder": "#FF6F61",
|
||||
"focus": "rgba(168, 156, 134, 0.50)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(168, 156, 134, 0.5)",
|
||||
"disabledBackground": "rgba(168, 156, 134, 0.06)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg,rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const desertBloomTheme: NewThemeOptions = {
|
||||
name: 'Desert bloom',
|
||||
colors: {
|
||||
mode: 'light',
|
||||
border: {
|
||||
weak: 'rgba(0, 0, 0, 0.12)',
|
||||
medium: 'rgba(0, 0, 0, 0.20)',
|
||||
strong: 'rgba(0, 0, 0, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: '#333333',
|
||||
secondary: '#555555',
|
||||
disabled: 'rgba(0, 0, 0, 0.5)',
|
||||
link: '#1A82E2',
|
||||
maxContrast: '#000000',
|
||||
},
|
||||
primary: {
|
||||
main: '#FF6F61',
|
||||
text: '#FE6F61',
|
||||
border: '#E55B4D',
|
||||
name: 'primary',
|
||||
shade: '#E55B4D',
|
||||
transparent: '#FF6F6126',
|
||||
contrastText: '#FFFFFF',
|
||||
borderTransparent: '#FF6F6140',
|
||||
},
|
||||
secondary: {
|
||||
main: '#FFFFFF',
|
||||
text: '#695f53',
|
||||
border: '#d9cec0',
|
||||
name: 'secondary',
|
||||
shade: '#d9cec0',
|
||||
transparent: '#FFFFFF26',
|
||||
contrastText: '#4c4339',
|
||||
borderTransparent: '#FFFFFF40',
|
||||
},
|
||||
info: {
|
||||
main: '#1A82E2',
|
||||
},
|
||||
success: {
|
||||
main: '#4CAF50',
|
||||
},
|
||||
warning: {
|
||||
main: '#FFC107',
|
||||
},
|
||||
background: {
|
||||
canvas: '#FFF8F0',
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#f9f3e8',
|
||||
elevated: '#FFFFFF',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(168, 156, 134, 0.12)',
|
||||
selected: 'rgba(168, 156, 134, 0.36)',
|
||||
selectedBorder: '#FF6F61',
|
||||
focus: 'rgba(168, 156, 134, 0.50)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(168, 156, 134, 0.5)',
|
||||
disabledBackground: 'rgba(168, 156, 134, 0.06)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg,rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default desertBloomTheme;
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "Gilded grove",
|
||||
"id": "gildedgrove",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(200, 200, 180, 0.12)",
|
||||
"medium": "rgba(200, 200, 180, 0.20)",
|
||||
"strong": "rgba(200, 200, 180, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "rgb(250, 250, 239)",
|
||||
"secondary": "rgba(200, 200, 180, 0.85)",
|
||||
"disabled": "rgba(200, 200, 180, 0.6)",
|
||||
"link": "#FEAC34",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FEAC34",
|
||||
"text": "#FFD783",
|
||||
"border": "#FFD783",
|
||||
"name": "primary",
|
||||
"shade": "rgb(255, 173, 80)",
|
||||
"transparent": "#FEAC3426",
|
||||
"contrastText": "#111614",
|
||||
"borderTransparent": "#FFD78340"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "rgba(200, 200, 180, 0.10)",
|
||||
"shade": "rgba(200, 200, 180, 0.14)",
|
||||
"transparent": "rgba(200, 200, 180, 0.08)",
|
||||
"text": "rgb(200, 200, 180)",
|
||||
"contrastText": "rgb(200, 200, 180)",
|
||||
"border": "rgba(200, 200, 180, 0.08)",
|
||||
"name": "secondary",
|
||||
"borderTransparent": "rgba(200, 200, 180, 0.25)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#111614",
|
||||
"primary": "#1d2220",
|
||||
"secondary": "#27312E",
|
||||
"elevated": "#27312E"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(200, 200, 180, 0.16)",
|
||||
"selected": "rgba(200, 200, 180, 0.12)",
|
||||
"selectedBorder": "#FEAC34",
|
||||
"focus": "rgba(200, 200, 180, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(200, 200, 180, 0.6)",
|
||||
"disabledBackground": "rgba(200, 200, 180, 0.04)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const gildedGroveTheme: NewThemeOptions = {
|
||||
name: 'Gilded grove',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(200, 200, 180, 0.12)',
|
||||
medium: 'rgba(200, 200, 180, 0.20)',
|
||||
strong: 'rgba(200, 200, 180, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: 'rgb(250, 250, 239)',
|
||||
secondary: 'rgba(200, 200, 180, 0.85)',
|
||||
disabled: 'rgba(200, 200, 180, 0.6)',
|
||||
link: '#FEAC34',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#FEAC34',
|
||||
text: '#FFD783',
|
||||
border: '#FFD783',
|
||||
name: 'primary',
|
||||
shade: 'rgb(255, 173, 80)',
|
||||
transparent: '#FEAC3426',
|
||||
contrastText: '#111614',
|
||||
borderTransparent: '#FFD78340',
|
||||
},
|
||||
secondary: {
|
||||
main: 'rgba(200, 200, 180, 0.10)',
|
||||
shade: 'rgba(200, 200, 180, 0.14)',
|
||||
transparent: 'rgba(200, 200, 180, 0.08)',
|
||||
text: 'rgb(200, 200, 180)',
|
||||
contrastText: 'rgb(200, 200, 180)',
|
||||
border: 'rgba(200, 200, 180, 0.08)',
|
||||
name: 'secondary',
|
||||
borderTransparent: 'rgba(200, 200, 180, 0.25)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#111614',
|
||||
primary: '#1d2220',
|
||||
secondary: '#27312E',
|
||||
elevated: '#27312E',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(200, 200, 180, 0.16)',
|
||||
selected: 'rgba(200, 200, 180, 0.12)',
|
||||
selectedBorder: '#FEAC34',
|
||||
focus: 'rgba(200, 200, 180, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(200, 200, 180, 0.6)',
|
||||
disabledBackground: 'rgba(200, 200, 180, 0.04)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default gildedGroveTheme;
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "Gloom",
|
||||
"id": "gloom",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(210, 210, 220, 0.12)",
|
||||
"medium": "rgba(210, 210, 220, 0.20)",
|
||||
"strong": "rgba(210, 210, 220, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "rgb(210, 210, 220)",
|
||||
"secondary": "rgba(210, 210, 220, 0.65)",
|
||||
"disabled": "rgba(210, 210, 220, 0.48)",
|
||||
"link": "#f99a5c",
|
||||
"maxContrast": "#FFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#ff934d",
|
||||
"text": "#f99a5c",
|
||||
"border": "#ff934d",
|
||||
"name": "primary"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "rgba(195, 195, 245, 0.10)",
|
||||
"shade": "rgba(195, 195, 245, 0.14)",
|
||||
"transparent": "rgba(195, 195, 245, 0.08)",
|
||||
"text": "rgba(195, 195, 245)",
|
||||
"contrastText": "rgb(195, 195, 245)",
|
||||
"border": "rgba(195, 195, 245, 0.08)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#000",
|
||||
"primary": "#121118",
|
||||
"secondary": "#211e28",
|
||||
"elevated": "#211e28"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(195, 195, 245, 0.07)",
|
||||
"selected": "rgba(195, 195, 245, 0.11)",
|
||||
"selectedBorder": "#ff934d",
|
||||
"focus": "rgba(195, 195, 245, 0.07)",
|
||||
"hoverOpacity": 0.05,
|
||||
"disabledText": "rgba(210, 210, 220, 0.48)",
|
||||
"disabledBackground": "rgba(210, 210, 220, 0.04)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
/**
|
||||
* Torkel's GrafanaCon theme
|
||||
* very WIP state
|
||||
*/
|
||||
|
||||
const whiteBase = `210, 210, 220`;
|
||||
const secondaryBase = `195, 195, 245`;
|
||||
|
||||
//const brandMain = '#3d71d9';
|
||||
//const brandText = '#6e9fff';
|
||||
const brandMain = '#ff934d';
|
||||
const brandText = '#f99a5c';
|
||||
const disabledText = `rgba(${whiteBase}, 0.48)`;
|
||||
|
||||
const gloomTheme: NewThemeOptions = {
|
||||
name: 'Gloom',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: `rgba(${whiteBase}, 0.12)`,
|
||||
medium: `rgba(${whiteBase}, 0.20)`,
|
||||
strong: `rgba(${whiteBase}, 0.30)`,
|
||||
},
|
||||
|
||||
text: {
|
||||
primary: `rgb(${whiteBase})`,
|
||||
secondary: `rgba(${whiteBase}, 0.65)`,
|
||||
disabled: disabledText,
|
||||
link: brandText,
|
||||
maxContrast: '#FFF',
|
||||
},
|
||||
|
||||
primary: {
|
||||
main: brandMain,
|
||||
text: brandText,
|
||||
border: brandMain,
|
||||
name: 'primary',
|
||||
},
|
||||
|
||||
secondary: {
|
||||
main: `rgba(${secondaryBase}, 0.10)`,
|
||||
shade: `rgba(${secondaryBase}, 0.14)`,
|
||||
transparent: `rgba(${secondaryBase}, 0.08)`,
|
||||
text: `rgba(${secondaryBase})`,
|
||||
contrastText: `rgb(${secondaryBase})`,
|
||||
border: `rgba(${secondaryBase}, 0.08)`,
|
||||
},
|
||||
|
||||
background: {
|
||||
canvas: '#000',
|
||||
primary: '#121118',
|
||||
secondary: '#211e28',
|
||||
elevated: '#211e28',
|
||||
},
|
||||
|
||||
action: {
|
||||
hover: `rgba(${secondaryBase}, 0.07)`,
|
||||
selected: `rgba(${secondaryBase}, 0.11)`,
|
||||
selectedBorder: brandMain,
|
||||
focus: `rgba(${secondaryBase}, 0.07)`,
|
||||
hoverOpacity: 0.05,
|
||||
disabledText: disabledText,
|
||||
disabledBackground: `rgba(${whiteBase}, 0.04)`,
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
|
||||
// gradients: {
|
||||
// brandHorizontal: 'linear-gradient(270deg, #ff934d 0%, #FEAC34 100%)',
|
||||
// brandVertical: 'linear-gradient(0.01deg, #ff934d 0.01%, #FEAC34 99.99%)',
|
||||
// },
|
||||
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default gloomTheme;
|
||||
@@ -1,12 +1,12 @@
|
||||
export { default as aubergine } from './aubergine.json';
|
||||
export { default as debug } from './debug.json';
|
||||
export { default as desertbloom } from './desertbloom.json';
|
||||
export { default as gildedgrove } from './gildedgrove.json';
|
||||
export { default as mars } from './mars.json';
|
||||
export { default as matrix } from './matrix.json';
|
||||
export { default as sapphiredusk } from './sapphiredusk.json';
|
||||
export { default as synthwave } from './synthwave.json';
|
||||
export { default as tron } from './tron.json';
|
||||
export { default as victorian } from './victorian.json';
|
||||
export { default as zen } from './zen.json';
|
||||
export { default as gloom } from './gloom.json';
|
||||
export { default as aubergine } from './aubergine';
|
||||
export { default as debug } from './debug';
|
||||
export { default as desertbloom } from './desertbloom';
|
||||
export { default as gildedgrove } from './gildedgrove';
|
||||
export { default as mars } from './mars';
|
||||
export { default as matrix } from './matrix';
|
||||
export { default as sapphiredusk } from './sapphiredusk';
|
||||
export { default as synthwave } from './synthwave';
|
||||
export { default as tron } from './tron';
|
||||
export { default as victorian } from './victorian';
|
||||
export { default as zen } from './zen';
|
||||
export { default as gloom } from './gloom';
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Mars",
|
||||
"id": "mars",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(210, 90, 60, 0.2)",
|
||||
"medium": "rgba(210, 90, 60, 0.35)",
|
||||
"strong": "rgba(210, 90, 60, 0.5)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#DDDDDD",
|
||||
"secondary": "#BBBBBB",
|
||||
"disabled": "rgba(221, 221, 221, 0.5)",
|
||||
"link": "#FF6F61",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FF6F61"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#6a2f2f",
|
||||
"text": "#BBBBBB",
|
||||
"border": "rgba(210, 90, 60, 0.2)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#3C1E1E",
|
||||
"primary": "#522626",
|
||||
"secondary": "#6A2F2F",
|
||||
"elevated": "#6A2F2F"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(210, 90, 60, 0.16)",
|
||||
"selected": "rgba(210, 90, 60, 0.12)",
|
||||
"selectedBorder": "#FF6F61",
|
||||
"focus": "rgba(210, 90, 60, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(221, 221, 221, 0.5)",
|
||||
"disabledBackground": "rgba(210, 90, 60, 0.08)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.05,
|
||||
"tonalOffset": 0.2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const marsTheme: NewThemeOptions = {
|
||||
name: 'Mars',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(210, 90, 60, 0.2)',
|
||||
medium: 'rgba(210, 90, 60, 0.35)',
|
||||
strong: 'rgba(210, 90, 60, 0.5)',
|
||||
},
|
||||
text: {
|
||||
primary: '#DDDDDD',
|
||||
secondary: '#BBBBBB',
|
||||
disabled: 'rgba(221, 221, 221, 0.5)',
|
||||
link: '#FF6F61',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#FF6F61',
|
||||
},
|
||||
secondary: {
|
||||
main: '#6a2f2f',
|
||||
text: '#BBBBBB',
|
||||
border: 'rgba(210, 90, 60, 0.2)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#3C1E1E',
|
||||
primary: '#522626',
|
||||
secondary: '#6A2F2F',
|
||||
elevated: '#6A2F2F',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(210, 90, 60, 0.16)',
|
||||
selected: 'rgba(210, 90, 60, 0.12)',
|
||||
selectedBorder: '#FF6F61',
|
||||
focus: 'rgba(210, 90, 60, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(221, 221, 221, 0.5)',
|
||||
disabledBackground: 'rgba(210, 90, 60, 0.08)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.05,
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
export default marsTheme;
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "Matrix",
|
||||
"id": "matrix",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"background": {
|
||||
"canvas": "#000000",
|
||||
"primary": "#020202",
|
||||
"secondary": "#080808",
|
||||
"elevated": "#080808"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#00c017",
|
||||
"secondary": "#008910",
|
||||
"disabled": "#006a0c",
|
||||
"link": "#00ff41",
|
||||
"maxContrast": "#00ff41"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#008f1144",
|
||||
"medium": "#008f1188",
|
||||
"strong": "#008910"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#008910"
|
||||
},
|
||||
"secondary": {
|
||||
"text": "#008910"
|
||||
},
|
||||
"gradients": {
|
||||
"brandVertical": "linear-gradient(0deg, #008910 0%, #00ff41 100%)",
|
||||
"brandHorizontal": "linear-gradient(90deg, #008910 0%, #00ff41 100%)"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"borderRadius": 0
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "monospace"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const matrixTheme: NewThemeOptions = {
|
||||
name: 'Matrix',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
background: {
|
||||
canvas: '#000000',
|
||||
primary: '#020202',
|
||||
secondary: '#080808',
|
||||
elevated: '#080808',
|
||||
},
|
||||
text: {
|
||||
primary: '#00c017',
|
||||
secondary: '#008910',
|
||||
disabled: '#006a0c',
|
||||
link: '#00ff41',
|
||||
maxContrast: '#00ff41',
|
||||
},
|
||||
border: {
|
||||
weak: '#008f1144',
|
||||
medium: '#008f1188',
|
||||
strong: '#008910',
|
||||
},
|
||||
primary: {
|
||||
main: '#008910',
|
||||
},
|
||||
secondary: {
|
||||
text: '#008910',
|
||||
},
|
||||
gradients: {
|
||||
brandVertical: 'linear-gradient(0deg, #008910 0%, #00ff41 100%)',
|
||||
brandHorizontal: 'linear-gradient(90deg, #008910 0%, #00ff41 100%)',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
};
|
||||
|
||||
export default matrixTheme;
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"name": "Sapphire dusk",
|
||||
"id": "sapphiredusk",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#232e47",
|
||||
"medium": "#2c3853",
|
||||
"strong": "#404d6b"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#FFFFFF",
|
||||
"secondary": "#bcccdd",
|
||||
"disabled": "#838da5",
|
||||
"link": "#93EBF0",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#93EBF0",
|
||||
"text": "#a8e9ed",
|
||||
"border": "#93ebf0",
|
||||
"name": "primary",
|
||||
"shade": "#c0f5d9",
|
||||
"transparent": "#93EBF029",
|
||||
"contrastText": "#111614",
|
||||
"borderTransparent": "#93ebf040"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#2c364f",
|
||||
"shade": "#36415e",
|
||||
"transparent": "rgba(200, 200, 180, 0.08)",
|
||||
"text": "#d1dfff",
|
||||
"contrastText": "#acfeff",
|
||||
"border": "rgba(200, 200, 180, 0.08)",
|
||||
"name": "secondary",
|
||||
"borderTransparent": "rgba(200, 200, 180, 0.25)"
|
||||
},
|
||||
"info": {
|
||||
"main": "#4d4593",
|
||||
"text": "#a8e9ed",
|
||||
"border": "#5d54a7"
|
||||
},
|
||||
"error": {
|
||||
"main": "#c63370"
|
||||
},
|
||||
"success": {
|
||||
"main": "#1A7F4B"
|
||||
},
|
||||
"warning": {
|
||||
"main": "#D448EA"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#1e273d",
|
||||
"primary": "#12192e",
|
||||
"secondary": "#212c47",
|
||||
"elevated": "#212c47"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#364057",
|
||||
"selected": "#364260",
|
||||
"selectedBorder": "#D448EA",
|
||||
"focus": "#364057",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "#838da5",
|
||||
"disabledBackground": "rgba(54, 64, 87, 0.2)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const sapphireDuskTheme: NewThemeOptions = {
|
||||
name: 'Sapphire dusk',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#232e47',
|
||||
medium: '#2c3853',
|
||||
strong: '#404d6b',
|
||||
},
|
||||
text: {
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#bcccdd',
|
||||
disabled: '#838da5',
|
||||
link: '#93EBF0',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#93EBF0',
|
||||
text: '#a8e9ed',
|
||||
border: '#93ebf0',
|
||||
name: 'primary',
|
||||
shade: '#c0f5d9',
|
||||
transparent: '#93EBF029',
|
||||
contrastText: '#111614',
|
||||
borderTransparent: '#93ebf040',
|
||||
},
|
||||
secondary: {
|
||||
main: '#2c364f',
|
||||
shade: '#36415e',
|
||||
transparent: 'rgba(200, 200, 180, 0.08)',
|
||||
text: '#d1dfff',
|
||||
contrastText: '#acfeff',
|
||||
border: 'rgba(200, 200, 180, 0.08)',
|
||||
name: 'secondary',
|
||||
borderTransparent: 'rgba(200, 200, 180, 0.25)',
|
||||
},
|
||||
info: {
|
||||
main: '#4d4593',
|
||||
text: '#a8e9ed',
|
||||
border: '#5d54a7',
|
||||
},
|
||||
error: {
|
||||
main: '#c63370',
|
||||
},
|
||||
success: {
|
||||
main: '#1A7F4B',
|
||||
},
|
||||
warning: {
|
||||
main: '#D448EA',
|
||||
},
|
||||
background: {
|
||||
canvas: '#1e273d',
|
||||
primary: '#12192e',
|
||||
secondary: '#212c47',
|
||||
elevated: '#212c47',
|
||||
},
|
||||
action: {
|
||||
hover: '#364057',
|
||||
selected: '#364260',
|
||||
selectedBorder: '#D448EA',
|
||||
focus: '#364057',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: '#838da5',
|
||||
disabledBackground: 'rgba(54, 64, 87, 0.2)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default sapphireDuskTheme;
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Synthwave",
|
||||
"id": "synthwave",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(255, 20, 147, 0.12)",
|
||||
"medium": "rgba(255, 20, 147, 0.20)",
|
||||
"strong": "rgba(255, 20, 147, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E0E0E0",
|
||||
"secondary": "rgba(224, 224, 224, 0.75)",
|
||||
"disabled": "rgba(224, 224, 224, 0.5)",
|
||||
"link": "#FF69B4",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FF1493"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#37183a",
|
||||
"text": "rgba(224, 224, 224, 0.75)",
|
||||
"border": "rgba(255, 20, 147, 0.10)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#1A1A2E",
|
||||
"primary": "#16213E",
|
||||
"secondary": "#0F3460",
|
||||
"elevated": "#0F3460"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(255, 20, 147, 0.16)",
|
||||
"selected": "rgba(255, 20, 147, 0.12)",
|
||||
"selectedBorder": "#FF1493",
|
||||
"focus": "rgba(255, 20, 147, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(224, 224, 224, 0.5)",
|
||||
"disabledBackground": "rgba(255, 20, 147, 0.08)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const synthwaveTheme: NewThemeOptions = {
|
||||
name: 'Synthwave',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(255, 20, 147, 0.12)',
|
||||
medium: 'rgba(255, 20, 147, 0.20)',
|
||||
strong: 'rgba(255, 20, 147, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: '#E0E0E0',
|
||||
secondary: 'rgba(224, 224, 224, 0.75)',
|
||||
disabled: 'rgba(224, 224, 224, 0.5)',
|
||||
link: '#FF69B4',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#FF1493',
|
||||
},
|
||||
secondary: {
|
||||
main: '#37183a',
|
||||
text: 'rgba(224, 224, 224, 0.75)',
|
||||
border: 'rgba(255, 20, 147, 0.10)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#1A1A2E',
|
||||
primary: '#16213E',
|
||||
secondary: '#0F3460',
|
||||
elevated: '#0F3460',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(255, 20, 147, 0.16)',
|
||||
selected: 'rgba(255, 20, 147, 0.12)',
|
||||
selectedBorder: '#FF1493',
|
||||
focus: 'rgba(255, 20, 147, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(224, 224, 224, 0.5)',
|
||||
disabledBackground: 'rgba(255, 20, 147, 0.08)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default synthwaveTheme;
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Tron",
|
||||
"id": "tron",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(0, 255, 255, 0.12)",
|
||||
"medium": "rgba(0, 255, 255, 0.20)",
|
||||
"strong": "rgba(0, 255, 255, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E0E0E0",
|
||||
"secondary": "rgba(224, 224, 224, 0.75)",
|
||||
"disabled": "rgba(224, 224, 224, 0.5)",
|
||||
"link": "#00FFFF",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#00FFFF"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#0b2e36",
|
||||
"text": "rgba(224, 224, 224, 0.75)",
|
||||
"border": "rgba(0, 255, 255, 0.10)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#0A0F18",
|
||||
"primary": "#0F1B2A",
|
||||
"secondary": "#152234",
|
||||
"elevated": "#152234"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(0, 255, 255, 0.16)",
|
||||
"selected": "rgba(0, 255, 255, 0.12)",
|
||||
"selectedBorder": "#00FFFF",
|
||||
"focus": "rgba(0, 255, 255, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(224, 224, 224, 0.5)",
|
||||
"disabledBackground": "rgba(0, 255, 255, 0.08)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.05,
|
||||
"tonalOffset": 0.2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const tronTheme: NewThemeOptions = {
|
||||
name: 'Tron',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(0, 255, 255, 0.12)',
|
||||
medium: 'rgba(0, 255, 255, 0.20)',
|
||||
strong: 'rgba(0, 255, 255, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: '#E0E0E0',
|
||||
secondary: 'rgba(224, 224, 224, 0.75)',
|
||||
disabled: 'rgba(224, 224, 224, 0.5)',
|
||||
link: '#00FFFF',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#00FFFF',
|
||||
},
|
||||
secondary: {
|
||||
main: '#0b2e36',
|
||||
text: 'rgba(224, 224, 224, 0.75)',
|
||||
border: 'rgba(0, 255, 255, 0.10)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#0A0F18',
|
||||
primary: '#0F1B2A',
|
||||
secondary: '#152234',
|
||||
elevated: '#152234',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(0, 255, 255, 0.16)',
|
||||
selected: 'rgba(0, 255, 255, 0.12)',
|
||||
selectedBorder: '#00FFFF',
|
||||
focus: 'rgba(0, 255, 255, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(224, 224, 224, 0.5)',
|
||||
disabledBackground: 'rgba(0, 255, 255, 0.08)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.05,
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
export default tronTheme;
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "Victorian",
|
||||
"id": "victorian",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#3A2C22",
|
||||
"medium": "#3A2C22",
|
||||
"strong": "#4B3D32"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#D9D0A2",
|
||||
"secondary": "#C4B89B",
|
||||
"disabled": "#A89F91",
|
||||
"link": "#C28A4D",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#C28A4D"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#3A2C22",
|
||||
"text": "#C4B89B",
|
||||
"border": "#4B3D32"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#1F1510",
|
||||
"primary": "#2C1A13",
|
||||
"secondary": "#402A21",
|
||||
"elevated": "#402A21"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#3A2C22",
|
||||
"selected": "#4B3D32",
|
||||
"selectedBorder": "#C28A4D",
|
||||
"focus": "#C28A4D",
|
||||
"hoverOpacity": 0.1,
|
||||
"disabledText": "#A89F91",
|
||||
"disabledBackground": "#402A21",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 4,
|
||||
"hoverFactor": 0.07,
|
||||
"tonalOffset": 0.15
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "\"Georgia\", \"Times New Roman\", serif",
|
||||
"fontFamilyMonospace": "'Courier New', monospace"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const victorianTheme: NewThemeOptions = {
|
||||
name: 'Victorian',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#3A2C22',
|
||||
medium: '#3A2C22',
|
||||
strong: '#4B3D32',
|
||||
},
|
||||
text: {
|
||||
primary: '#D9D0A2',
|
||||
secondary: '#C4B89B',
|
||||
disabled: '#A89F91',
|
||||
link: '#C28A4D',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#C28A4D',
|
||||
},
|
||||
secondary: {
|
||||
main: '#3A2C22',
|
||||
text: '#C4B89B',
|
||||
border: '#4B3D32',
|
||||
},
|
||||
background: {
|
||||
canvas: '#1F1510',
|
||||
primary: '#2C1A13',
|
||||
secondary: '#402A21',
|
||||
elevated: '#402A21',
|
||||
},
|
||||
action: {
|
||||
hover: '#3A2C22',
|
||||
selected: '#4B3D32',
|
||||
selectedBorder: '#C28A4D',
|
||||
focus: '#C28A4D',
|
||||
hoverOpacity: 0.1,
|
||||
disabledText: '#A89F91',
|
||||
disabledBackground: '#402A21',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)',
|
||||
},
|
||||
contrastThreshold: 4,
|
||||
hoverFactor: 0.07,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Georgia", "Times New Roman", serif',
|
||||
fontFamilyMonospace: "'Courier New', monospace",
|
||||
},
|
||||
};
|
||||
|
||||
export default victorianTheme;
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "Zen",
|
||||
"id": "zen",
|
||||
"colors": {
|
||||
"mode": "light",
|
||||
"text": {
|
||||
"primary": "#333333",
|
||||
"secondary": "#666666",
|
||||
"disabled": "#B8B8B8",
|
||||
"link": "#4F9F6E",
|
||||
"maxContrast": "#000000"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#B1B7B3",
|
||||
"medium": "#A2A8A2",
|
||||
"strong": "#7C7F7A"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#6D8E6D"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#E0E0E0",
|
||||
"text": "#666666",
|
||||
"border": "#A2A8A2"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#F4F4F4",
|
||||
"primary": "#E9E9E9",
|
||||
"secondary": "#D8D8D8",
|
||||
"elevated": "#E9E9E9"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#D1D1D1",
|
||||
"selected": "#B8B8B8",
|
||||
"selectedBorder": "#88B88B",
|
||||
"hoverOpacity": 0.1,
|
||||
"focus": "#D1D1D1",
|
||||
"disabledBackground": "#E0E0E0",
|
||||
"disabledText": "#B8B8B8",
|
||||
"disabledOpacity": 0.5
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const zenTheme: NewThemeOptions = {
|
||||
name: 'Zen',
|
||||
colors: {
|
||||
mode: 'light',
|
||||
text: {
|
||||
primary: '#333333',
|
||||
secondary: '#666666',
|
||||
disabled: '#B8B8B8',
|
||||
link: '#4F9F6E',
|
||||
maxContrast: '#000000',
|
||||
},
|
||||
border: {
|
||||
weak: '#B1B7B3',
|
||||
medium: '#A2A8A2',
|
||||
strong: '#7C7F7A',
|
||||
},
|
||||
primary: {
|
||||
main: '#6D8E6D',
|
||||
},
|
||||
secondary: {
|
||||
main: '#E0E0E0',
|
||||
text: '#666666',
|
||||
border: '#A2A8A2',
|
||||
},
|
||||
background: {
|
||||
canvas: '#F4F4F4',
|
||||
primary: '#E9E9E9',
|
||||
secondary: '#D8D8D8',
|
||||
elevated: '#E9E9E9',
|
||||
},
|
||||
action: {
|
||||
hover: '#D1D1D1',
|
||||
selected: '#B8B8B8',
|
||||
selectedBorder: '#88B88B',
|
||||
hoverOpacity: 0.1,
|
||||
focus: '#D1D1D1',
|
||||
disabledBackground: '#E0E0E0',
|
||||
disabledText: '#B8B8B8',
|
||||
disabledOpacity: 0.5,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
export default zenTheme;
|
||||
@@ -1,5 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { GrafanaTheme } from '../types/theme';
|
||||
|
||||
import { ThemeBreakpoints } from './breakpoints';
|
||||
@@ -37,36 +35,27 @@ export interface GrafanaTheme2 {
|
||||
flags: {};
|
||||
}
|
||||
|
||||
export const ThemeRichColorInputSchema = z.object({
|
||||
/** color intent (primary, secondary, info, error, etc) */
|
||||
name: z.string().optional(),
|
||||
/** Main color */
|
||||
main: z.string().optional(),
|
||||
/** Used for hover */
|
||||
shade: z.string().optional(),
|
||||
/** Used for text */
|
||||
text: z.string().optional(),
|
||||
/** Used for borders */
|
||||
border: z.string().optional(),
|
||||
/** Used subtly colored backgrounds */
|
||||
transparent: z.string().optional(),
|
||||
/** Used for weak colored borders like larger alert/banner boxes and smaller badges and tags */
|
||||
borderTransparent: z.string().optional(),
|
||||
/** Text color for text ontop of main */
|
||||
contrastText: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ThemeRichColorSchema = ThemeRichColorInputSchema.required();
|
||||
|
||||
/** @alpha */
|
||||
export type ThemeRichColor = z.infer<typeof ThemeRichColorSchema>;
|
||||
export interface ThemeRichColor {
|
||||
/** color intent (primary, secondary, info, error, etc) */
|
||||
name: string;
|
||||
/** Main color */
|
||||
main: string;
|
||||
/** Used for hover */
|
||||
shade: string;
|
||||
/** Used for text */
|
||||
text: string;
|
||||
/** Used for borders */
|
||||
border: string;
|
||||
/** Used subtly colored backgrounds */
|
||||
transparent: string;
|
||||
/** Used for weak colored borders like larger alert/banner boxes and smaller badges and tags */
|
||||
borderTransparent: string;
|
||||
/** Text color for text ontop of main */
|
||||
contrastText: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type DeepRequired<T> = Required<{
|
||||
[P in keyof T]: T[P] extends Required<T[P]> ? T[P] : DeepRequired<T[P]>;
|
||||
}>;
|
||||
|
||||
@@ -649,6 +649,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
rolePickerDrawer?: boolean;
|
||||
/**
|
||||
* Enable sprinkles on unified storage search
|
||||
*/
|
||||
unifiedStorageSearchSprinkles?: boolean;
|
||||
/**
|
||||
* Pick the dual write mode from database configs
|
||||
*/
|
||||
managedDualWriter?: boolean;
|
||||
|
||||
@@ -9,4 +9,5 @@
|
||||
* and be subject to the standard policies
|
||||
*/
|
||||
|
||||
export { default as themeJsonSchema } from './themes/schema.generated.json';
|
||||
// This is a dummy export so typescript doesn't error importing an "empty module"
|
||||
export const unstable = {};
|
||||
|
||||
@@ -1,500 +0,0 @@
|
||||
/**
|
||||
* Types for Scopes API - matching @grafana/data types
|
||||
*/
|
||||
|
||||
export interface ScopeFilter {
|
||||
key: string;
|
||||
value: string;
|
||||
operator: 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match';
|
||||
}
|
||||
|
||||
export interface ScopeSpec {
|
||||
title: string;
|
||||
filters: ScopeFilter[];
|
||||
}
|
||||
|
||||
export interface Scope {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: ScopeSpec;
|
||||
}
|
||||
|
||||
export interface ScopeNodeSpec {
|
||||
nodeType: 'container' | 'leaf';
|
||||
title: string;
|
||||
description?: string;
|
||||
disableMultiSelect?: boolean;
|
||||
linkType?: 'scope';
|
||||
linkId?: string;
|
||||
parentName: string;
|
||||
}
|
||||
|
||||
export interface ScopeNode {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: ScopeNodeSpec;
|
||||
}
|
||||
|
||||
export interface ScopeDashboardBindingSpec {
|
||||
dashboard: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface ScopeDashboardBindingStatus {
|
||||
dashboardTitle: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
export interface ScopeDashboardBinding {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: ScopeDashboardBindingSpec;
|
||||
status: ScopeDashboardBindingStatus;
|
||||
}
|
||||
|
||||
export interface ScopeNavigation {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: {
|
||||
url: string;
|
||||
scope: string;
|
||||
subScope?: string;
|
||||
preLoadSubScopeChildren?: boolean;
|
||||
expandOnLoad?: boolean;
|
||||
disableSubScopeSelection?: boolean;
|
||||
};
|
||||
status: {
|
||||
title: string;
|
||||
groups?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export const MOCK_SCOPES: Scope[] = [
|
||||
{
|
||||
metadata: { name: 'cloud' },
|
||||
spec: {
|
||||
title: 'Cloud',
|
||||
filters: [{ key: 'cloud', value: '.*', operator: 'regex-match' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'dev' },
|
||||
spec: {
|
||||
title: 'Dev',
|
||||
filters: [{ key: 'cloud', value: 'dev', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'ops' },
|
||||
spec: {
|
||||
title: 'Ops',
|
||||
filters: [{ key: 'cloud', value: 'ops', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'prod' },
|
||||
spec: {
|
||||
title: 'Prod',
|
||||
filters: [{ key: 'cloud', value: 'prod', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'grafana' },
|
||||
spec: {
|
||||
title: 'Grafana',
|
||||
filters: [{ key: 'app', value: 'grafana', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'mimir' },
|
||||
spec: {
|
||||
title: 'Mimir',
|
||||
filters: [{ key: 'app', value: 'mimir', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'loki' },
|
||||
spec: {
|
||||
title: 'Loki',
|
||||
filters: [{ key: 'app', value: 'loki', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'tempo' },
|
||||
spec: {
|
||||
title: 'Tempo',
|
||||
filters: [{ key: 'app', value: 'tempo', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'dev-env' },
|
||||
spec: {
|
||||
title: 'Development',
|
||||
filters: [{ key: 'environment', value: 'dev', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'prod-env' },
|
||||
spec: {
|
||||
title: 'Production',
|
||||
filters: [{ key: 'environment', value: 'prod', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const dashboardBindingsGenerator = (
|
||||
scopes: string[],
|
||||
dashboards: Array<{ dashboardTitle: string; dashboardKey?: string; groups?: string[] }>
|
||||
) =>
|
||||
scopes.reduce<ScopeDashboardBinding[]>((scopeAcc, scopeTitle) => {
|
||||
const scope = scopeTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
|
||||
|
||||
return [
|
||||
...scopeAcc,
|
||||
...dashboards.reduce<ScopeDashboardBinding[]>((acc, { dashboardTitle, groups, dashboardKey }, idx) => {
|
||||
dashboardKey = dashboardKey ?? dashboardTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
|
||||
const group = !groups
|
||||
? ''
|
||||
: groups.length === 1
|
||||
? groups[0] === ''
|
||||
? ''
|
||||
: `${groups[0].toLowerCase().replaceAll(' ', '-').replaceAll('/', '-')}-`
|
||||
: `multiple${idx}-`;
|
||||
const dashboard = `${group}${dashboardKey}`;
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
metadata: { name: `${scope}-${dashboard}` },
|
||||
spec: {
|
||||
dashboard,
|
||||
scope,
|
||||
},
|
||||
status: {
|
||||
dashboardTitle,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []),
|
||||
];
|
||||
}, []);
|
||||
|
||||
export const MOCK_SCOPE_DASHBOARD_BINDINGS: ScopeDashboardBinding[] = [
|
||||
...dashboardBindingsGenerator(
|
||||
['Grafana'],
|
||||
[
|
||||
{ dashboardTitle: 'Data Sources', groups: ['General'] },
|
||||
{ dashboardTitle: 'Usage', groups: ['General'] },
|
||||
{ dashboardTitle: 'Frontend Errors', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Frontend Logs', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Backend Errors', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Backend Logs', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Usage Overview', groups: ['Usage'] },
|
||||
{ dashboardTitle: 'Data Sources', groups: ['Usage'] },
|
||||
{ dashboardTitle: 'Stats', groups: ['Usage'] },
|
||||
{ dashboardTitle: 'Overview', groups: [''] },
|
||||
{ dashboardTitle: 'Frontend' },
|
||||
{ dashboardTitle: 'Stats' },
|
||||
]
|
||||
),
|
||||
...dashboardBindingsGenerator(
|
||||
['Loki', 'Tempo', 'Mimir'],
|
||||
[
|
||||
{ dashboardTitle: 'Ingester', groups: ['Components', 'Investigations'] },
|
||||
{ dashboardTitle: 'Distributor', groups: ['Components', 'Investigations'] },
|
||||
{ dashboardTitle: 'Compacter', groups: ['Components', 'Investigations'] },
|
||||
{ dashboardTitle: 'Datasource Errors', groups: ['Observability', 'Investigations'] },
|
||||
{ dashboardTitle: 'Datasource Logs', groups: ['Observability', 'Investigations'] },
|
||||
{ dashboardTitle: 'Overview' },
|
||||
{ dashboardTitle: 'Stats', dashboardKey: 'another-stats' },
|
||||
]
|
||||
),
|
||||
...dashboardBindingsGenerator(
|
||||
['Dev', 'Ops', 'Prod'],
|
||||
[
|
||||
{ dashboardTitle: 'Overview', groups: ['Cardinality Management'] },
|
||||
{ dashboardTitle: 'Metrics', groups: ['Cardinality Management'] },
|
||||
{ dashboardTitle: 'Labels', groups: ['Cardinality Management'] },
|
||||
{ dashboardTitle: 'Overview', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Data Sources', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Query Errors', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Alertmanager', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Metrics Ingestion', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Billing/Usage' },
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
export const MOCK_NODES: ScopeNode[] = [
|
||||
{
|
||||
metadata: { name: 'applications' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Applications',
|
||||
description: 'Application Scopes',
|
||||
parentName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Cloud',
|
||||
description: 'Cloud Scopes',
|
||||
disableMultiSelect: true,
|
||||
linkType: 'scope',
|
||||
linkId: 'cloud',
|
||||
parentName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-grafana' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Grafana',
|
||||
description: 'Grafana',
|
||||
linkType: 'scope',
|
||||
linkId: 'grafana',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-mimir' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Mimir',
|
||||
description: 'Mimir',
|
||||
linkType: 'scope',
|
||||
linkId: 'mimir',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-loki' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Loki',
|
||||
description: 'Loki',
|
||||
linkType: 'scope',
|
||||
linkId: 'loki',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-tempo' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Tempo',
|
||||
description: 'Tempo',
|
||||
linkType: 'scope',
|
||||
linkId: 'tempo',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Cloud',
|
||||
description: 'Application/Cloud Scopes',
|
||||
linkType: 'scope',
|
||||
linkId: 'cloud',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud-dev' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Dev',
|
||||
description: 'Dev',
|
||||
linkType: 'scope',
|
||||
linkId: 'dev',
|
||||
parentName: 'applications-cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud-ops' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Ops',
|
||||
description: 'Ops',
|
||||
linkType: 'scope',
|
||||
linkId: 'ops',
|
||||
parentName: 'applications-cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud-prod' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Prod',
|
||||
description: 'Prod',
|
||||
linkType: 'scope',
|
||||
linkId: 'prod',
|
||||
parentName: 'applications-cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-dev' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Dev',
|
||||
description: 'Dev',
|
||||
linkType: 'scope',
|
||||
linkId: 'dev',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-ops' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Ops',
|
||||
description: 'Ops',
|
||||
linkType: 'scope',
|
||||
linkId: 'ops',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-prod' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Prod',
|
||||
description: 'Prod',
|
||||
linkType: 'scope',
|
||||
linkId: 'prod',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Applications',
|
||||
description: 'Cloud/Application Scopes',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-grafana' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Grafana',
|
||||
description: 'Grafana',
|
||||
linkType: 'scope',
|
||||
linkId: 'grafana',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-mimir' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Mimir',
|
||||
description: 'Mimir',
|
||||
linkType: 'scope',
|
||||
linkId: 'mimir',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-loki' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Loki',
|
||||
description: 'Loki',
|
||||
linkType: 'scope',
|
||||
linkId: 'loki',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-tempo' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Tempo',
|
||||
description: 'Tempo',
|
||||
linkType: 'scope',
|
||||
linkId: 'tempo',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'environments' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Environments',
|
||||
description: 'Environment Scopes',
|
||||
disableMultiSelect: true,
|
||||
parentName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'environments-dev' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Development',
|
||||
description: 'Development Environment',
|
||||
linkType: 'scope',
|
||||
linkId: 'dev-env',
|
||||
parentName: 'environments',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'environments-prod' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Production',
|
||||
description: 'Production Environment',
|
||||
linkType: 'scope',
|
||||
linkId: 'prod-env',
|
||||
parentName: 'environments',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_SUB_SCOPE_MIMIR_ITEMS: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'mimir-item-1' },
|
||||
spec: {
|
||||
scope: 'mimir',
|
||||
url: '/d/mimir-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboard 1',
|
||||
groups: ['General'],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'mimir-item-2' },
|
||||
spec: {
|
||||
scope: 'mimir',
|
||||
url: '/d/mimir-dashboard-2',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboard 2',
|
||||
groups: ['Observability'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_SUB_SCOPE_LOKI_ITEMS: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'loki-item-1' },
|
||||
spec: {
|
||||
scope: 'loki',
|
||||
url: '/d/loki-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Loki Dashboard 1',
|
||||
groups: ['General'],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -12,7 +12,6 @@ import appPlatformDashboardv0alpha1Handlers from './apis/dashboard.grafana.app/v
|
||||
import appPlatformDashboardv1beta1Handlers from './apis/dashboard.grafana.app/v1beta1/handlers';
|
||||
import appPlatformFolderv1beta1Handlers from './apis/folder.grafana.app/v1beta1/handlers';
|
||||
import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/handlers';
|
||||
import appPlatformScopev0alpha1Handlers from './apis/scope.grafana.app/v0alpha1/handlers';
|
||||
|
||||
const allHandlers: HttpHandler[] = [
|
||||
// Legacy handlers
|
||||
@@ -30,7 +29,6 @@ const allHandlers: HttpHandler[] = [
|
||||
...appPlatformFolderv1beta1Handlers,
|
||||
...appPlatformIamv0alpha1Handlers,
|
||||
...appPlatformCollectionsv1alpha1Handlers,
|
||||
...appPlatformScopev0alpha1Handlers,
|
||||
];
|
||||
|
||||
export default allHandlers;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
MOCK_NODES,
|
||||
MOCK_SCOPES,
|
||||
MOCK_SCOPE_DASHBOARD_BINDINGS,
|
||||
MOCK_SUB_SCOPE_LOKI_ITEMS,
|
||||
MOCK_SUB_SCOPE_MIMIR_ITEMS,
|
||||
ScopeNavigation,
|
||||
} from '../../../../fixtures/scopes';
|
||||
import { getErrorResponse } from '../../../helpers';
|
||||
|
||||
const API_BASE = '/apis/scope.grafana.app/v0alpha1/namespaces/:namespace';
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/scopes/:name
|
||||
*
|
||||
* Fetches a single scope by name.
|
||||
*/
|
||||
const getScopeHandler = () =>
|
||||
http.get<{ namespace: string; name: string }>(`${API_BASE}/scopes/:name`, ({ params }) => {
|
||||
const { name } = params;
|
||||
const scope = MOCK_SCOPES.find((s) => s.metadata.name === name);
|
||||
|
||||
if (!scope) {
|
||||
return HttpResponse.json(getErrorResponse(`scopes.scope.grafana.app "${name}" not found`, 404), {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return HttpResponse.json(scope);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/scopenodes/:name
|
||||
*
|
||||
* Fetches a single scope node by name.
|
||||
*/
|
||||
const getScopeNodeHandler = () =>
|
||||
http.get<{ namespace: string; name: string }>(`${API_BASE}/scopenodes/:name`, ({ params }) => {
|
||||
const { name } = params;
|
||||
const node = MOCK_NODES.find((n) => n.metadata.name === name);
|
||||
|
||||
if (!node) {
|
||||
return HttpResponse.json(getErrorResponse(`scopenodes.scope.grafana.app "${name}" not found`, 404), {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return HttpResponse.json(node);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_node_children
|
||||
*
|
||||
* Finds scope node children based on parent and query filters.
|
||||
*/
|
||||
const findScopeNodeChildrenHandler = () =>
|
||||
http.get(`${API_BASE}/find/scope_node_children`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const parent = url.searchParams.get('parent') ?? '';
|
||||
const query = url.searchParams.get('query') ?? '';
|
||||
const limitParam = url.searchParams.get('limit');
|
||||
const names = url.searchParams.getAll('names');
|
||||
|
||||
let filtered = MOCK_NODES.filter(
|
||||
(node) => node.spec.parentName === parent && node.spec.title.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
if (names.length > 0) {
|
||||
filtered = MOCK_NODES.filter((node) => names.includes(node.metadata.name));
|
||||
}
|
||||
|
||||
if (limitParam) {
|
||||
const limit = parseInt(limitParam, 10);
|
||||
filtered = filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
items: filtered,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_dashboard_bindings
|
||||
*
|
||||
* Finds scope dashboard bindings for the given scope names.
|
||||
*/
|
||||
const findScopeDashboardBindingsHandler = () =>
|
||||
http.get(`${API_BASE}/find/scope_dashboard_bindings`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const scopeNames = url.searchParams.getAll('scope');
|
||||
|
||||
const bindings = MOCK_SCOPE_DASHBOARD_BINDINGS.filter((b) => scopeNames.includes(b.spec.scope));
|
||||
|
||||
return HttpResponse.json({
|
||||
items: bindings,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_navigations
|
||||
*
|
||||
* Finds scope navigations for the given scope names.
|
||||
*/
|
||||
const findScopeNavigationsHandler = () =>
|
||||
http.get(`${API_BASE}/find/scope_navigations`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const scopeNames = url.searchParams.getAll('scope');
|
||||
|
||||
let items: ScopeNavigation[] = [];
|
||||
|
||||
if (scopeNames.includes('mimir')) {
|
||||
items = [...items, ...MOCK_SUB_SCOPE_MIMIR_ITEMS];
|
||||
}
|
||||
if (scopeNames.includes('loki')) {
|
||||
items = [...items, ...MOCK_SUB_SCOPE_LOKI_ITEMS];
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
items,
|
||||
});
|
||||
});
|
||||
|
||||
export default [
|
||||
getScopeHandler(),
|
||||
getScopeNodeHandler(),
|
||||
findScopeNodeChildrenHandler(),
|
||||
findScopeDashboardBindingsHandler(),
|
||||
findScopeNavigationsHandler(),
|
||||
];
|
||||
@@ -2,12 +2,3 @@ import { wellFormedTree } from './fixtures/folders';
|
||||
|
||||
export const getFolderFixtures = wellFormedTree;
|
||||
export { MOCK_TEAMS, MOCK_TEAM_GROUPS } from './fixtures/teams';
|
||||
export {
|
||||
MOCK_SCOPES,
|
||||
MOCK_NODES,
|
||||
MOCK_SCOPE_DASHBOARD_BINDINGS,
|
||||
MOCK_SUB_SCOPE_MIMIR_ITEMS,
|
||||
MOCK_SUB_SCOPE_LOKI_ITEMS,
|
||||
} from './fixtures/scopes';
|
||||
export { default as allHandlers } from './handlers/all-handlers';
|
||||
export { default as scopeHandlers } from './handlers/apis/scope.grafana.app/v0alpha1/handlers';
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
@@ -68,7 +69,6 @@ require (
|
||||
github.com/onsi/gomega v1.36.2 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
@@ -60,6 +66,103 @@ const (
|
||||
Mode5
|
||||
)
|
||||
|
||||
type NamespacedKVStore interface {
|
||||
Get(ctx context.Context, key string) (string, bool, error)
|
||||
Set(ctx context.Context, key, value string) error
|
||||
}
|
||||
|
||||
type ServerLockService interface {
|
||||
LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
|
||||
}
|
||||
|
||||
func SetDualWritingMode(
|
||||
ctx context.Context,
|
||||
kvs NamespacedKVStore,
|
||||
cfg *SyncerConfig,
|
||||
metrics *DualWriterMetrics,
|
||||
) (DualWriterMode, error) {
|
||||
if cfg == nil {
|
||||
return Mode0, errors.New("syncer config is nil")
|
||||
}
|
||||
// Mode0 means no DualWriter
|
||||
if cfg.Mode == Mode0 {
|
||||
return Mode0, nil
|
||||
}
|
||||
|
||||
toMode := map[string]DualWriterMode{
|
||||
// It is not possible to initialize a mode 0 dual writer. Mode 0 represents
|
||||
// writing to legacy storage without Unified Storage enabled.
|
||||
"1": Mode1,
|
||||
"2": Mode2,
|
||||
"3": Mode3,
|
||||
"4": Mode4,
|
||||
"5": Mode5,
|
||||
}
|
||||
errDualWriterSetCurrentMode := errors.New("failed to set current dual writing mode")
|
||||
|
||||
// Use entity name as key
|
||||
kvMode, ok, err := kvs.Get(ctx, cfg.Kind)
|
||||
if err != nil {
|
||||
return Mode0, errors.New("failed to fetch current dual writing mode")
|
||||
}
|
||||
|
||||
currentMode, exists := toMode[kvMode]
|
||||
|
||||
// If the mode does not exist in our mapping, we log an error.
|
||||
if !exists && ok {
|
||||
// Only log if "ok" because initially all instances will have mode unset for playlists.
|
||||
klog.Infof("invalid dual writing mode for %s mode: %v", cfg.Kind, kvMode)
|
||||
}
|
||||
|
||||
// If the mode does not exist in our mapping, and we also didn't find an entry for this kind, fallback.
|
||||
if !exists || !ok {
|
||||
// Default to mode 1
|
||||
currentMode = Mode1
|
||||
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(currentMode)); err != nil {
|
||||
return Mode0, errDualWriterSetCurrentMode
|
||||
}
|
||||
}
|
||||
|
||||
isUpgradeToReadUnifiedMode := currentMode < Mode3 && cfg.Mode >= Mode3
|
||||
if !isUpgradeToReadUnifiedMode {
|
||||
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil {
|
||||
return Mode0, errDualWriterSetCurrentMode
|
||||
}
|
||||
return cfg.Mode, nil
|
||||
}
|
||||
|
||||
// If SkipDataSync is enabled, we can set the mode directly without running the syncer.
|
||||
if cfg.SkipDataSync {
|
||||
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil {
|
||||
return Mode0, errDualWriterSetCurrentMode
|
||||
}
|
||||
return cfg.Mode, nil
|
||||
}
|
||||
|
||||
// Transitioning to Mode3 or higher from Mode0, Mode1, or Mode2.
|
||||
// We need to run the syncer in the current mode before we can upgrade to Mode3 or higher.
|
||||
cfgModeTmp := cfg.Mode
|
||||
// Before running the sync, set the syncer config to the current mode, as we have to run the syncer
|
||||
// once in the current active mode before we can upgrade.
|
||||
cfg.Mode = currentMode
|
||||
syncOk, err := runDataSyncer(ctx, cfg, metrics)
|
||||
// Once we are done with running the syncer, we can change the mode back on the config to the desired one.
|
||||
cfg.Mode = cfgModeTmp
|
||||
if err != nil {
|
||||
klog.Error("data syncer failed for mode:", kvMode, "err", err)
|
||||
return currentMode, nil
|
||||
}
|
||||
if !syncOk {
|
||||
klog.Info("data syncer not ok for mode:", kvMode)
|
||||
return currentMode, nil
|
||||
}
|
||||
// If sync is successful, update the mode to the desired one.
|
||||
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil {
|
||||
return Mode0, errDualWriterSetCurrentMode
|
||||
}
|
||||
return cfg.Mode, nil
|
||||
}
|
||||
|
||||
// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage)
|
||||
func Compare(objA, objB runtime.Object) bool {
|
||||
if objA == nil || objB == nil {
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
type syncItem struct {
|
||||
name string
|
||||
objStorage runtime.Object
|
||||
objLegacy runtime.Object
|
||||
accessorStorage utils.GrafanaMetaAccessor
|
||||
accessorLegacy utils.GrafanaMetaAccessor
|
||||
}
|
||||
|
||||
type SyncerConfig struct {
|
||||
Kind string
|
||||
RequestInfo *request.RequestInfo
|
||||
|
||||
Mode DualWriterMode
|
||||
LegacyStorage Storage
|
||||
Storage Storage
|
||||
ServerLockService ServerLockService
|
||||
SkipDataSync bool
|
||||
|
||||
DataSyncerInterval time.Duration
|
||||
DataSyncerRecordsLimit int
|
||||
}
|
||||
|
||||
func (s *SyncerConfig) Validate() error {
|
||||
if s == nil {
|
||||
return fmt.Errorf("syncer config is nil")
|
||||
}
|
||||
if s.Kind == "" {
|
||||
return fmt.Errorf("kind must be specified")
|
||||
}
|
||||
if s.RequestInfo == nil {
|
||||
return fmt.Errorf("requestInfo must be specified")
|
||||
}
|
||||
if s.ServerLockService == nil {
|
||||
return fmt.Errorf("serverLockService must be specified")
|
||||
}
|
||||
if s.Storage == nil {
|
||||
return fmt.Errorf("storage must be specified")
|
||||
}
|
||||
if s.LegacyStorage == nil {
|
||||
return fmt.Errorf("legacy storage must be specified")
|
||||
}
|
||||
if s.DataSyncerInterval == 0 {
|
||||
s.DataSyncerInterval = time.Hour
|
||||
}
|
||||
if s.DataSyncerRecordsLimit == 0 {
|
||||
s.DataSyncerRecordsLimit = 1000
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer, syncing the data
|
||||
// from the hosted grafana backend into the unified storage backend. This is run in the grafana instance.
|
||||
func StartPeriodicDataSyncer(ctx context.Context, cfg *SyncerConfig, metrics *DualWriterMetrics) error {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid syncer config: %w", err)
|
||||
}
|
||||
|
||||
log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", cfg.Mode, "resource", cfg.Kind)
|
||||
|
||||
log.Info("Starting periodic data syncer")
|
||||
|
||||
// run in background
|
||||
go func() {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
timeWindow := 600 // 600 seconds (10 minutes)
|
||||
jitterSeconds := r.Int63n(int64(timeWindow))
|
||||
log.Info("data syncer scheduled", "starting time", time.Now().Add(time.Second*time.Duration(jitterSeconds)))
|
||||
time.Sleep(time.Second * time.Duration(jitterSeconds))
|
||||
|
||||
// run it immediately
|
||||
syncOK, err := runDataSyncer(ctx, cfg, metrics)
|
||||
log.Info("data syncer finished", "syncOK", syncOK, "error", err)
|
||||
|
||||
ticker := time.NewTicker(cfg.DataSyncerInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
syncOK, err = runDataSyncer(ctx, cfg, metrics)
|
||||
log.Info("data syncer finished", "syncOK", syncOK, ", error", err)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// runDataSyncer will ensure that data between legacy storage and unified storage are in sync.
|
||||
// The sync implementation depends on the DualWriter mode
|
||||
func runDataSyncer(ctx context.Context, cfg *SyncerConfig, metrics *DualWriterMetrics) (bool, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return false, fmt.Errorf("invalid syncer config: %w", err)
|
||||
}
|
||||
// ensure that execution takes no longer than necessary
|
||||
timeout := cfg.DataSyncerInterval - time.Minute
|
||||
ctx, cancelFn := context.WithTimeout(ctx, timeout)
|
||||
defer cancelFn()
|
||||
|
||||
// implementation depends on the current DualWriter mode
|
||||
switch cfg.Mode {
|
||||
case Mode1, Mode2:
|
||||
return legacyToUnifiedStorageDataSyncer(ctx, cfg, metrics)
|
||||
default:
|
||||
klog.Info("data syncer not implemented for mode:", cfg.Mode)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig, metrics *DualWriterMetrics) (bool, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return false, fmt.Errorf("invalid syncer config: %w", err)
|
||||
}
|
||||
|
||||
log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", cfg.Mode, "resource", cfg.Kind)
|
||||
|
||||
everythingSynced := false
|
||||
outOfSync := 0
|
||||
syncSuccess := 0
|
||||
syncErr := 0
|
||||
|
||||
maxInterval := cfg.DataSyncerInterval + 5*time.Minute
|
||||
|
||||
var errSync error
|
||||
|
||||
// LockExecuteAndRelease ensures that just a single Grafana server acquires a lock at a time
|
||||
// The parameter 'maxInterval' is a timeout safeguard, if the LastExecution in the
|
||||
// database is older than maxInterval, we will assume the lock as timeouted. The 'maxInterval' parameter should be so long
|
||||
// that is impossible for 2 processes to run at the same time.
|
||||
err := cfg.ServerLockService.LockExecuteAndRelease(ctx, fmt.Sprintf("legacyToUnifiedStorageDataSyncer-%d-%s", cfg.Mode, cfg.Kind), maxInterval, func(context.Context) {
|
||||
log.Info("starting legacyToUnifiedStorageDataSyncer")
|
||||
startSync := time.Now()
|
||||
|
||||
ctx = klog.NewContext(ctx, log)
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, 0)
|
||||
ctx = request.WithNamespace(ctx, cfg.RequestInfo.Namespace)
|
||||
ctx = request.WithRequestInfo(ctx, cfg.RequestInfo)
|
||||
|
||||
storageList, err := getList(ctx, cfg.Storage, &metainternalversion.ListOptions{
|
||||
Limit: int64(cfg.DataSyncerRecordsLimit),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err, "unable to extract list from storage")
|
||||
return
|
||||
}
|
||||
|
||||
if len(storageList) >= cfg.DataSyncerRecordsLimit {
|
||||
errSync = fmt.Errorf("unified storage has more than %d records. Aborting sync", cfg.DataSyncerRecordsLimit)
|
||||
log.Error(errSync, "Unified storage has more records to be synced than allowed")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("got items from unified storage", "items", len(storageList))
|
||||
|
||||
legacyList, err := getList(ctx, cfg.LegacyStorage, &metainternalversion.ListOptions{
|
||||
Limit: int64(cfg.DataSyncerRecordsLimit),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err, "unable to extract list from legacy storage")
|
||||
return
|
||||
}
|
||||
log.Info("got items from legacy storage", "items", len(legacyList))
|
||||
|
||||
itemsByName := map[string]syncItem{}
|
||||
for _, obj := range legacyList {
|
||||
accessor, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
log.Error(err, "error retrieving accessor data for object from legacy storage")
|
||||
continue
|
||||
}
|
||||
name := accessor.GetName()
|
||||
|
||||
item := itemsByName[name]
|
||||
item.name = name
|
||||
item.objLegacy = obj
|
||||
item.accessorLegacy = accessor
|
||||
itemsByName[name] = item
|
||||
}
|
||||
|
||||
for _, obj := range storageList {
|
||||
accessor, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
log.Error(err, "error retrieving accessor data for object from storage")
|
||||
continue
|
||||
}
|
||||
name := accessor.GetName()
|
||||
|
||||
item := itemsByName[name]
|
||||
item.name = name
|
||||
item.objStorage = obj
|
||||
item.accessorStorage = accessor
|
||||
itemsByName[name] = item
|
||||
}
|
||||
log.Info("got list of items to be synced", "items", len(itemsByName))
|
||||
|
||||
for name, item := range itemsByName {
|
||||
// upsert if:
|
||||
// - existing in both legacy and storage, but objects are different, or
|
||||
// - if it's missing from storage
|
||||
if item.objLegacy != nil &&
|
||||
(item.objStorage == nil || !Compare(item.objLegacy, item.objStorage)) {
|
||||
outOfSync++
|
||||
|
||||
if item.objStorage != nil {
|
||||
item.accessorLegacy.SetResourceVersion(item.accessorStorage.GetResourceVersion())
|
||||
item.accessorLegacy.SetUID(item.accessorStorage.GetUID())
|
||||
|
||||
log.Info("updating item on unified storage", "name", name)
|
||||
} else {
|
||||
item.accessorLegacy.SetResourceVersion("")
|
||||
item.accessorLegacy.SetUID("")
|
||||
|
||||
log.Info("inserting item on unified storage", "name", name)
|
||||
}
|
||||
|
||||
objInfo := rest.DefaultUpdatedObjectInfo(item.objLegacy, []rest.TransformFunc{}...)
|
||||
res, _, err := cfg.Storage.Update(ctx,
|
||||
name,
|
||||
objInfo,
|
||||
func(ctx context.Context, obj runtime.Object) error { return nil },
|
||||
func(ctx context.Context, obj, old runtime.Object) error { return nil },
|
||||
true, // force creation
|
||||
&metav1.UpdateOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
log.WithValues("object", res).Error(err, "could not update in storage")
|
||||
syncErr++
|
||||
} else {
|
||||
syncSuccess++
|
||||
}
|
||||
}
|
||||
|
||||
// delete if object does not exists on legacy but exists on storage
|
||||
if item.objLegacy == nil && item.objStorage != nil {
|
||||
outOfSync++
|
||||
|
||||
ctx = request.WithRequestInfo(ctx, &request.RequestInfo{
|
||||
APIGroup: cfg.RequestInfo.APIGroup,
|
||||
Resource: cfg.RequestInfo.Resource,
|
||||
Name: name,
|
||||
Namespace: cfg.RequestInfo.Namespace,
|
||||
})
|
||||
|
||||
log.Info("deleting item from unified storage", "name", name)
|
||||
|
||||
deletedS, _, err := cfg.Storage.Delete(ctx, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
log.WithValues("objectList", deletedS).Error(err, "could not delete from storage")
|
||||
syncErr++
|
||||
continue
|
||||
}
|
||||
|
||||
syncSuccess++
|
||||
}
|
||||
}
|
||||
|
||||
everythingSynced = outOfSync == syncSuccess
|
||||
|
||||
metrics.recordDataSyncerOutcome(cfg.Mode, cfg.Kind, everythingSynced)
|
||||
metrics.recordDataSyncerDuration(err != nil, cfg.Mode, cfg.Kind, startSync)
|
||||
|
||||
log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced)
|
||||
})
|
||||
|
||||
if errSync != nil {
|
||||
err = errSync
|
||||
}
|
||||
|
||||
return everythingSynced, err
|
||||
}
|
||||
|
||||
func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) {
|
||||
var allItems []runtime.Object
|
||||
|
||||
for {
|
||||
if int64(len(allItems)) >= listOptions.Limit {
|
||||
return nil, fmt.Errorf("list has more than %d records. Aborting sync", listOptions.Limit)
|
||||
}
|
||||
|
||||
ll, err := obj.List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, err := meta.ExtractList(ll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allItems = append(allItems, items...)
|
||||
|
||||
// Get continue token from the list metadata.
|
||||
listMeta, err := meta.ListAccessor(ll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If no continue token, we're done paginating.
|
||||
if listMeta.GetContinue() == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// Set continue token for next page.
|
||||
listOptions.Continue = listMeta.GetContinue()
|
||||
}
|
||||
|
||||
return allItems, nil
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/apis/example"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
var legacyObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
var legacyObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
var legacyObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
var legacyObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
|
||||
var legacyObj2WithHostname = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{Hostname: "hostname"}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
|
||||
var storageObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
var storageObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
var storageObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
var storageObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
|
||||
|
||||
var legacyListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
|
||||
Items: []example.Pod{
|
||||
*legacyObj1,
|
||||
*legacyObj2,
|
||||
*legacyObj3,
|
||||
}}
|
||||
|
||||
var legacyListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
|
||||
Items: []example.Pod{
|
||||
*legacyObj1,
|
||||
*legacyObj2,
|
||||
*legacyObj3,
|
||||
*legacyObj4,
|
||||
}}
|
||||
|
||||
var legacyListWith3itemsObj2IsDifferent = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
|
||||
Items: []example.Pod{
|
||||
*legacyObj1,
|
||||
*legacyObj2WithHostname,
|
||||
*legacyObj3,
|
||||
}}
|
||||
|
||||
var storageListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
|
||||
Items: []example.Pod{
|
||||
*storageObj1,
|
||||
*storageObj2,
|
||||
*storageObj3,
|
||||
}}
|
||||
|
||||
var storageListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
|
||||
Items: []example.Pod{
|
||||
*storageObj1,
|
||||
*storageObj2,
|
||||
*storageObj3,
|
||||
*storageObj4,
|
||||
}}
|
||||
|
||||
var storageListWith3itemsMissingFoo2 = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
|
||||
Items: []example.Pod{
|
||||
*storageObj1,
|
||||
*storageObj3,
|
||||
*storageObj4,
|
||||
}}
|
||||
|
||||
func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) {
|
||||
type testCase struct {
|
||||
setupLegacyFn func(m *MockStorage)
|
||||
setupStorageFn func(m *MockStorage)
|
||||
name string
|
||||
expectedOutcome bool
|
||||
wantErr bool
|
||||
}
|
||||
tests :=
|
||||
[]testCase{
|
||||
{
|
||||
name: "both stores are in sync",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
|
||||
},
|
||||
expectedOutcome: true,
|
||||
},
|
||||
{
|
||||
name: "both stores are in sync - fail to list from legacy",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, errors.New("error"))
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
|
||||
},
|
||||
expectedOutcome: false,
|
||||
},
|
||||
{
|
||||
name: "both stores are in sync - fail to list from storage",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil).Maybe()
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, errors.New("error"))
|
||||
},
|
||||
expectedOutcome: false,
|
||||
},
|
||||
{
|
||||
name: "storage is missing 1 entry (foo4)",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
|
||||
m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
||||
},
|
||||
expectedOutcome: true,
|
||||
},
|
||||
{
|
||||
name: "storage needs to be update (foo2 is different)",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3itemsObj2IsDifferent, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
|
||||
m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
||||
},
|
||||
expectedOutcome: true,
|
||||
},
|
||||
{
|
||||
name: "storage is missing 1 entry (foo4) - fail to upsert",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
|
||||
m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error"))
|
||||
},
|
||||
expectedOutcome: false,
|
||||
},
|
||||
{
|
||||
name: "storage has an extra 1 entry (foo4)",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil)
|
||||
m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
||||
},
|
||||
expectedOutcome: true,
|
||||
},
|
||||
{
|
||||
name: "storage has an extra 1 entry (foo4) - fail to delete",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil)
|
||||
m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error"))
|
||||
},
|
||||
expectedOutcome: false,
|
||||
},
|
||||
{
|
||||
name: "storage is missing 1 entry (foo3) and has an extra 1 entry (foo4)",
|
||||
setupLegacyFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
|
||||
},
|
||||
setupStorageFn: func(m *MockStorage) {
|
||||
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3itemsMissingFoo2, nil)
|
||||
m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
||||
m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
||||
},
|
||||
expectedOutcome: true,
|
||||
},
|
||||
}
|
||||
|
||||
// mode 1
|
||||
for _, tt := range tests {
|
||||
t.Run("Mode-1-"+tt.name, func(t *testing.T) {
|
||||
ls := NewMockStorage(t)
|
||||
us := NewMockStorage(t)
|
||||
|
||||
if tt.setupLegacyFn != nil {
|
||||
tt.setupLegacyFn(ls)
|
||||
}
|
||||
if tt.setupStorageFn != nil {
|
||||
tt.setupStorageFn(us)
|
||||
}
|
||||
|
||||
outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), &SyncerConfig{
|
||||
Mode: Mode1,
|
||||
LegacyStorage: ls,
|
||||
Storage: us,
|
||||
Kind: "test.kind",
|
||||
ServerLockService: &fakeServerLock{},
|
||||
RequestInfo: &request.RequestInfo{},
|
||||
|
||||
DataSyncerRecordsLimit: 1000,
|
||||
DataSyncerInterval: time.Hour,
|
||||
}, NewDualWriterMetrics(nil))
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedOutcome, outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// mode 2
|
||||
for _, tt := range tests {
|
||||
t.Run("Mode-2-"+tt.name, func(t *testing.T) {
|
||||
ls := NewMockStorage(t)
|
||||
us := NewMockStorage(t)
|
||||
|
||||
if tt.setupLegacyFn != nil {
|
||||
tt.setupLegacyFn(ls)
|
||||
}
|
||||
if tt.setupStorageFn != nil {
|
||||
tt.setupStorageFn(us)
|
||||
}
|
||||
|
||||
outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), &SyncerConfig{
|
||||
Mode: Mode2,
|
||||
LegacyStorage: ls,
|
||||
Storage: us,
|
||||
Kind: "test.kind",
|
||||
ServerLockService: &fakeServerLock{},
|
||||
RequestInfo: &request.RequestInfo{},
|
||||
|
||||
DataSyncerRecordsLimit: 1000,
|
||||
DataSyncerInterval: time.Hour,
|
||||
}, NewDualWriterMetrics(nil))
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedOutcome, outcome)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,124 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/apis/example"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
var now = time.Now()
|
||||
|
||||
var exampleObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", CreationTimestamp: metav1.Time{}, GenerateName: "foo"}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: now}}}
|
||||
var anotherObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "2", GenerateName: "foo"}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: now}}}
|
||||
var exampleList = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{*exampleObj}}
|
||||
var anotherList = &example.PodList{Items: []example.Pod{*anotherObj}}
|
||||
|
||||
func TestSetDualWritingMode(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
kvStore *fakeNamespacedKV
|
||||
desiredMode DualWriterMode
|
||||
expectedMode DualWriterMode
|
||||
expectedKVMode string
|
||||
skipDataSync bool
|
||||
serverLockError error
|
||||
}
|
||||
tests :=
|
||||
[]testCase{
|
||||
{
|
||||
name: "should return a mode 2 dual writer when mode 2 is set as the desired mode",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode2,
|
||||
expectedMode: Mode2,
|
||||
expectedKVMode: "2",
|
||||
},
|
||||
{
|
||||
name: "should return a mode 1 dual writer when mode 1 is set as the desired mode",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode1,
|
||||
expectedMode: Mode1,
|
||||
expectedKVMode: "1",
|
||||
},
|
||||
{
|
||||
name: "should return mode 3 as desired mode when current mode is > 3",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "5"}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode3,
|
||||
expectedMode: Mode3,
|
||||
expectedKVMode: "3",
|
||||
},
|
||||
{
|
||||
name: "should return mode 3 as desired mode when current mode is 2",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode3,
|
||||
expectedMode: Mode3,
|
||||
expectedKVMode: "3",
|
||||
},
|
||||
{
|
||||
name: "should default to mode 0 if there is no desired mode",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode0,
|
||||
expectedMode: Mode0,
|
||||
expectedKVMode: "",
|
||||
},
|
||||
{
|
||||
name: "should keep mode2 when trying to go from mode2 to mode3 and the server lock service returns an error",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode3,
|
||||
expectedMode: Mode2,
|
||||
expectedKVMode: "2",
|
||||
serverLockError: fmt.Errorf("lock already exists"),
|
||||
},
|
||||
{
|
||||
name: "should keep mode2 when trying to go from mode2 to mode3 and migration is disabled",
|
||||
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
|
||||
desiredMode: Mode3,
|
||||
expectedMode: Mode3,
|
||||
expectedKVMode: "3",
|
||||
skipDataSync: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
us := NewMockStorage(t)
|
||||
us.On("List", mock.Anything, mock.Anything).Return(anotherList, nil).Maybe()
|
||||
us.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Maybe()
|
||||
us.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Maybe()
|
||||
|
||||
ls := NewMockStorage(t)
|
||||
ls.On("List", mock.Anything, mock.Anything).Return(exampleList, nil).Maybe()
|
||||
|
||||
serverLockSvc := &fakeServerLock{
|
||||
err: tt.serverLockError,
|
||||
}
|
||||
|
||||
dwMode, err := SetDualWritingMode(context.Background(), tt.kvStore, &SyncerConfig{
|
||||
LegacyStorage: ls,
|
||||
Storage: us,
|
||||
Kind: "playlist.grafana.app/playlists",
|
||||
Mode: tt.desiredMode,
|
||||
SkipDataSync: tt.skipDataSync,
|
||||
ServerLockService: serverLockSvc,
|
||||
RequestInfo: &request.RequestInfo{},
|
||||
|
||||
DataSyncerRecordsLimit: 1000,
|
||||
DataSyncerInterval: time.Hour,
|
||||
}, NewDualWriterMetrics(nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedMode, dwMode)
|
||||
|
||||
kvMode, _, err := tt.kvStore.Get(context.Background(), "playlist.grafana.app/playlists")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedKVMode, kvMode, "expected mode for playlist.grafana.app/playlists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
var exampleObjGen1 = &example.Pod{ObjectMeta: metav1.ObjectMeta{Generation: 1}, Spec: example.PodSpec{Hostname: "one"}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Unix(0, 0)}}}
|
||||
@@ -57,3 +163,30 @@ func TestCompare(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeNamespacedKV struct {
|
||||
namespace string
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (f *fakeNamespacedKV) Get(ctx context.Context, key string) (string, bool, error) {
|
||||
val, ok := f.data[key]
|
||||
return val, ok, nil
|
||||
}
|
||||
|
||||
func (f *fakeNamespacedKV) Set(ctx context.Context, key, value string) error {
|
||||
f.data[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeServerLock struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeServerLock) LockExecuteAndRelease(ctx context.Context, actionName string, duration time.Duration, fn func(ctx context.Context)) error {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
fn(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type DualWriterMetrics struct {
|
||||
// DualWriterSyncerDuration is a metric summary for dual writer sync duration per mode
|
||||
syncer *prometheus.HistogramVec
|
||||
// DualWriterDataSyncerOutcome is a metric summary for dual writer data syncer outcome comparison between the 2 stores per mode
|
||||
syncerOutcome *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
func NewDualWriterMetrics(reg prometheus.Registerer) *DualWriterMetrics {
|
||||
return &DualWriterMetrics{
|
||||
syncer: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "dual_writer_data_syncer_duration_seconds",
|
||||
Help: "Histogram for the runtime of dual writer data syncer duration per mode",
|
||||
Namespace: "grafana",
|
||||
NativeHistogramBucketFactor: 1.1,
|
||||
}, []string{"is_error", "mode", "resource"}),
|
||||
|
||||
syncerOutcome: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "dual_writer_data_syncer_outcome",
|
||||
Help: "Histogram for the runtime of dual writer data syncer outcome comparison between the 2 stores per mode",
|
||||
Namespace: "grafana",
|
||||
NativeHistogramBucketFactor: 1.1,
|
||||
}, []string{"mode", "resource"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DualWriterMetrics) recordDataSyncerDuration(isError bool, mode DualWriterMode, resource string, startFrom time.Time) {
|
||||
duration := time.Since(startFrom).Seconds()
|
||||
m.syncer.WithLabelValues(strconv.FormatBool(isError), fmt.Sprintf("%d", mode), resource).Observe(duration)
|
||||
}
|
||||
|
||||
func (m *DualWriterMetrics) recordDataSyncerOutcome(mode DualWriterMode, resource string, synced bool) {
|
||||
var observeValue float64
|
||||
if !synced {
|
||||
observeValue = 1
|
||||
}
|
||||
m.syncerOutcome.WithLabelValues(fmt.Sprintf("%d", mode), resource).Observe(observeValue)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||
_ "github.com/Azure/go-autorest/autorest"
|
||||
_ "github.com/Azure/go-autorest/autorest/adal"
|
||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
_ "github.com/beevik/etree"
|
||||
_ "github.com/blugelabs/bluge"
|
||||
_ "github.com/blugelabs/bluge_segment_api"
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
type ListTeamBindingsQuery struct {
|
||||
UID string
|
||||
OrgID int64
|
||||
TeamUID string
|
||||
UserUID string
|
||||
Pagination common.Pagination
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ WHERE
|
||||
{{ if .Query.UID }}
|
||||
AND tm.uid = {{ .Arg .Query.UID }}
|
||||
{{ end }}
|
||||
{{ if .Query.TeamUID }}
|
||||
AND t.uid = {{ .Arg .Query.TeamUID }}
|
||||
{{ end }}
|
||||
{{ if .Query.UserUID }}
|
||||
AND u.uid = {{ .Arg .Query.UserUID }}
|
||||
{{ end }}
|
||||
{{- if .Query.Pagination.Continue }}
|
||||
AND tm.id >= {{ .Arg .Query.Pagination.Continue }}
|
||||
{{- end }}
|
||||
|
||||
@@ -357,7 +357,9 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamsAPIGroup(opts builder.AP
|
||||
|
||||
func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts builder.APIGroupOptions, storage map[string]rest.Storage, enableZanzanaSync bool) error {
|
||||
teamBindingResource := iamv0.TeamBindingResourceInfo
|
||||
teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter)
|
||||
teamBindingUniStore, err := grafanaregistry.NewRegistryStoreWithSelectableFields(opts.Scheme, teamBindingResource, opts.OptsGetter, grafanaregistry.SelectableFieldsOptions{
|
||||
GetAttrs: teambinding.GetAttrs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package teambinding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
)
|
||||
|
||||
// GetTeamBindingSelectableFields returns a field set that can be used for field selectors.
|
||||
func GetTeamBindingSelectableFields(obj *iamv0alpha1.TeamBinding) fields.Set {
|
||||
objectMetaFields := generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true)
|
||||
|
||||
// Using the generated schema to get selectable fields dynamically
|
||||
schema := iamv0alpha1.TeamBindingSchema()
|
||||
specificFields := fields.Set{}
|
||||
|
||||
for _, selectableField := range schema.SelectableFields() {
|
||||
val, err := selectableField.FieldValueFunc(obj)
|
||||
if err != nil {
|
||||
val = ""
|
||||
}
|
||||
specificFields[selectableField.FieldSelector] = val
|
||||
}
|
||||
|
||||
return generic.MergeFieldsSets(objectMetaFields, specificFields)
|
||||
}
|
||||
|
||||
// GetAttrs returns labels and fields of a TeamBinding object.
|
||||
// This is used by the storage layer for filtering.
|
||||
func GetAttrs(o runtime.Object) (labels.Set, fields.Set, error) {
|
||||
tb, ok := o.(*iamv0alpha1.TeamBinding)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("provided object must be of type *TeamBinding")
|
||||
}
|
||||
|
||||
return labels.Set(tb.Labels), GetTeamBindingSelectableFields(tb), nil
|
||||
}
|
||||
@@ -276,9 +276,20 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{
|
||||
query := legacy.ListTeamBindingsQuery{
|
||||
Pagination: common.PaginationFromListOptions(options),
|
||||
})
|
||||
}
|
||||
|
||||
if options.FieldSelector != nil {
|
||||
if name, ok := options.FieldSelector.RequiresExactMatch("spec.teamRef.name"); ok {
|
||||
query.TeamUID = name
|
||||
}
|
||||
if name, ok := options.FieldSelector.RequiresExactMatch("spec.subject.name"); ok {
|
||||
query.UserUID = name
|
||||
}
|
||||
}
|
||||
|
||||
res, err := l.store.ListTeamBindings(ctx, ns, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/dskit/flagext"
|
||||
@@ -16,15 +15,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
@@ -116,25 +111,14 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
|
||||
Help: "Time spent executing requests to resource server.",
|
||||
Buckets: prometheus.ExponentialBuckets(0.008, 4, 7),
|
||||
}, []string{"operation", "status_code"})
|
||||
factoryRequestRetries := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "resource_server_client_request_retries_total",
|
||||
Help: "Total number of retries for requests to the resource server.",
|
||||
}, []string{"operation"})
|
||||
|
||||
factory := ringclient.PoolInstFunc(func(inst ring.InstanceDesc) (ringclient.PoolClient, error) {
|
||||
unaryInterceptors, streamInterceptors := grpcclient.Instrument(factoryRequestDuration)
|
||||
|
||||
// Add retry interceptors for transient connection issues
|
||||
unaryInterceptors = append(unaryInterceptors, ringClientRetryInterceptor())
|
||||
unaryInterceptors = append(unaryInterceptors, ringClientRetryInstrument(factoryRequestRetries))
|
||||
|
||||
opts, err := clientCfg.DialOption(unaryInterceptors, streamInterceptors, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, connectionBackoffOptions())
|
||||
|
||||
conn, err := grpc.NewClient(inst.Addr, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial resource server %s %s: %s", inst.Id, inst.Addr, err)
|
||||
@@ -151,40 +135,3 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
|
||||
|
||||
return ringclient.NewPool(resource.RingName, poolCfg, nil, factory, clientsCount, log)
|
||||
}
|
||||
|
||||
// ringClientRetryInterceptor creates an interceptor to perform retries for unary methods.
|
||||
// It retries on ResourceExhausted and Unavailable codes, which are typical for
|
||||
// transient connection issues and rate limiting.
|
||||
func ringClientRetryInterceptor() grpc.UnaryClientInterceptor {
|
||||
return grpc_retry.UnaryClientInterceptor(
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.1)),
|
||||
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
|
||||
)
|
||||
}
|
||||
|
||||
// ringClientRetryInstrument creates an interceptor to count retry attempts for metrics.
|
||||
func ringClientRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, resp interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
// We can tell if a call is a retry by checking the retry attempt metadata.
|
||||
attempt, err := strconv.Atoi(metautils.ExtractOutgoing(ctx).Get(grpc_retry.AttemptMetadataKey))
|
||||
if err == nil && attempt > 0 {
|
||||
metric.WithLabelValues(method).Inc()
|
||||
}
|
||||
return invoker(ctx, method, req, resp, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// connectionBackoffOptions configures connection backoff parameters for faster recovery from
|
||||
// transient connection failures (e.g., during pod restarts).
|
||||
func connectionBackoffOptions() grpc.DialOption {
|
||||
return grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -835,7 +835,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
builderMetrics := builder.ProvideBuilderMetrics(registerer)
|
||||
backend := auditing.ProvideNoopBackend()
|
||||
policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider()
|
||||
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, sqlStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleProvider)
|
||||
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1503,7 +1503,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
builderMetrics := builder.ProvideBuilderMetrics(registerer)
|
||||
backend := auditing.ProvideNoopBackend()
|
||||
policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider()
|
||||
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, sqlStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleProvider)
|
||||
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ kubernetesPlaylists = true
|
||||
|
||||
[unified_storage.playlists.playlist.grafana.app]
|
||||
dualWriterMode = 2
|
||||
dualWriterPeriodicDataSyncJobEnabled = true
|
||||
```
|
||||
|
||||
This will create a development kubeconfig and start a parallel ssl listener. It can be registered by
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -20,10 +21,13 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
type LegacyStorageGetterFunc func(schema.GroupVersionResource) grafanarest.Storage
|
||||
|
||||
type LegacyStorageProvider interface {
|
||||
GetLegacyStorage(schema.GroupVersionResource) grafanarest.Storage
|
||||
}
|
||||
@@ -43,6 +47,11 @@ type AppInstallerConfig struct {
|
||||
AllowedV0Alpha1Resources []string
|
||||
}
|
||||
|
||||
// serverLock interface defines a lock mechanism for executing actions with a timeout
|
||||
type serverLock interface {
|
||||
LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
|
||||
}
|
||||
|
||||
// AddToScheme adds app installer schemas to the runtime scheme
|
||||
func AddToScheme(
|
||||
appInstallers []appsdkapiserver.AppInstaller,
|
||||
@@ -130,7 +139,11 @@ func InstallAPIs(
|
||||
server *genericapiserver.GenericAPIServer,
|
||||
restOpsGetter generic.RESTOptionsGetter,
|
||||
storageOpts *grafanaapiserveroptions.StorageOptions,
|
||||
kvStore grafanarest.NamespacedKVStore,
|
||||
lock serverLock,
|
||||
namespaceMapper request.NamespaceMapper,
|
||||
dualWriteService dualwrite.Service,
|
||||
dualWriterMetrics *grafanarest.DualWriterMetrics,
|
||||
builderMetrics *builder.BuilderMetrics,
|
||||
apiResourceConfig *serverstore.ResourceConfig,
|
||||
) error {
|
||||
@@ -143,7 +156,11 @@ func InstallAPIs(
|
||||
installer: installer,
|
||||
storageOpts: storageOpts,
|
||||
restOptionsGetter: restOpsGetter,
|
||||
kvStore: kvStore,
|
||||
lock: lock,
|
||||
namespaceMapper: namespaceMapper,
|
||||
dualWriteService: dualWriteService,
|
||||
dualWriterMetrics: dualWriterMetrics,
|
||||
builderMetrics: builderMetrics,
|
||||
apiResourceConfig: apiResourceConfig,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
@@ -30,7 +31,11 @@ type serverWrapper struct {
|
||||
installer appsdkapiserver.AppInstaller
|
||||
restOptionsGetter generic.RESTOptionsGetter
|
||||
storageOpts *grafanaapiserveroptions.StorageOptions
|
||||
kvStore grafanarest.NamespacedKVStore
|
||||
lock serverLock
|
||||
namespaceMapper request.NamespaceMapper
|
||||
dualWriteService dualwrite.Service
|
||||
dualWriterMetrics *grafanarest.DualWriterMetrics
|
||||
builderMetrics *builder.BuilderMetrics
|
||||
apiResourceConfig *serverstorage.ResourceConfig
|
||||
}
|
||||
@@ -59,11 +64,16 @@ func (s *serverWrapper) InstallAPIGroup(apiGroupInfo *genericapiserver.APIGroupI
|
||||
if unifiedStorage, ok := storage.(grafanarest.Storage); ok {
|
||||
log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath)
|
||||
storage, err = NewDualWriter(
|
||||
s.ctx,
|
||||
gr,
|
||||
s.storageOpts,
|
||||
legacyProvider.GetLegacyStorage(gr.WithVersion(v)),
|
||||
unifiedStorage,
|
||||
s.kvStore,
|
||||
s.lock,
|
||||
s.namespaceMapper,
|
||||
s.dualWriteService,
|
||||
s.dualWriterMetrics,
|
||||
s.builderMetrics,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
package appinstaller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
k8srequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
// NewDualWriter creates a dual writer for the given group resource using the provided configuration
|
||||
func NewDualWriter(
|
||||
_ context.Context,
|
||||
gr schema.GroupResource,
|
||||
storageOpts *options.StorageOptions,
|
||||
legacy grafanarest.Storage,
|
||||
storage grafanarest.Storage,
|
||||
kvStore grafanarest.NamespacedKVStore,
|
||||
lock serverLock,
|
||||
namespaceMapper request.NamespaceMapper,
|
||||
dualWriteService dualwrite.Service,
|
||||
dualWriterMetrics *grafanarest.DualWriterMetrics,
|
||||
builderMetrics *builder.BuilderMetrics,
|
||||
) (grafanarest.Storage, error) {
|
||||
// Dashboards + Folders may be managed (depends on feature toggles and database state)
|
||||
@@ -29,14 +40,54 @@ func NewDualWriter(
|
||||
// when missing this will default to mode zero (legacy only)
|
||||
var mode = grafanarest.DualWriterMode(0)
|
||||
|
||||
var (
|
||||
dualWriterPeriodicDataSyncJobEnabled bool
|
||||
dualWriterMigrationDataSyncDisabled bool
|
||||
dataSyncerInterval = time.Hour
|
||||
dataSyncerRecordsLimit = 1000
|
||||
)
|
||||
|
||||
resourceConfig, resourceExists := storageOpts.UnifiedStorageConfig[key]
|
||||
if resourceExists {
|
||||
mode = resourceConfig.DualWriterMode
|
||||
dualWriterPeriodicDataSyncJobEnabled = resourceConfig.DualWriterPeriodicDataSyncJobEnabled
|
||||
dualWriterMigrationDataSyncDisabled = resourceConfig.DualWriterMigrationDataSyncDisabled
|
||||
dataSyncerInterval = resourceConfig.DataSyncerInterval
|
||||
dataSyncerRecordsLimit = resourceConfig.DataSyncerRecordsLimit
|
||||
}
|
||||
|
||||
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode)
|
||||
// Force using storage only -- regardless of internal synchronization state
|
||||
if mode == grafanarest.Mode5 {
|
||||
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode, grafanarest.Mode5)
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
switch mode {
|
||||
// Moving from one version to the next can only happen after the previous step has
|
||||
// successfully synchronized.
|
||||
requestInfo := getRequestInfo(gr, namespaceMapper)
|
||||
|
||||
syncerCfg := &grafanarest.SyncerConfig{
|
||||
Kind: key,
|
||||
RequestInfo: requestInfo,
|
||||
Mode: mode,
|
||||
SkipDataSync: dualWriterMigrationDataSyncDisabled,
|
||||
LegacyStorage: legacy,
|
||||
Storage: storage,
|
||||
ServerLockService: lock,
|
||||
DataSyncerInterval: dataSyncerInterval,
|
||||
DataSyncerRecordsLimit: dataSyncerRecordsLimit,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// This also sets the currentMode on the syncer config.
|
||||
currentMode, err := grafanarest.SetDualWritingMode(ctx, kvStore, syncerCfg, dualWriterMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode, currentMode)
|
||||
|
||||
switch currentMode {
|
||||
case grafanarest.Mode0:
|
||||
return legacy, nil
|
||||
case grafanarest.Mode4, grafanarest.Mode5:
|
||||
@@ -44,5 +95,26 @@ func NewDualWriter(
|
||||
default:
|
||||
}
|
||||
|
||||
return dualwrite.NewStaticStorage(gr, mode, legacy, storage)
|
||||
if dualWriterPeriodicDataSyncJobEnabled {
|
||||
// The mode might have changed in SetDualWritingMode, so apply current mode first.
|
||||
syncerCfg.Mode = currentMode
|
||||
if err := grafanarest.StartPeriodicDataSyncer(ctx, syncerCfg, dualWriterMetrics); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// when unable to use
|
||||
if currentMode != mode {
|
||||
klog.Warningf("Requested DualWrite mode: %d, but using %d for %+v", mode, currentMode, gr)
|
||||
}
|
||||
return dualwrite.NewStaticStorage(gr, currentMode, legacy, storage)
|
||||
}
|
||||
|
||||
func getRequestInfo(gr schema.GroupResource, namespaceMapper request.NamespaceMapper) *k8srequest.RequestInfo {
|
||||
return &k8srequest.RequestInfo{
|
||||
APIGroup: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Name: "",
|
||||
Namespace: namespaceMapper(int64(1)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
k8srequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
@@ -29,6 +32,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apiserver/auditing"
|
||||
"github.com/grafana/grafana/pkg/apiserver/endpoints/filters"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -249,6 +253,19 @@ func SetupConfig(
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServerLockService interface {
|
||||
LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
|
||||
}
|
||||
|
||||
func getRequestInfo(gr schema.GroupResource, namespaceMapper request.NamespaceMapper) *k8srequest.RequestInfo {
|
||||
return &k8srequest.RequestInfo{
|
||||
APIGroup: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Name: "",
|
||||
Namespace: namespaceMapper(int64(1)),
|
||||
}
|
||||
}
|
||||
|
||||
func InstallAPIs(
|
||||
scheme *runtime.Scheme,
|
||||
codecs serializer.CodecFactory,
|
||||
@@ -257,9 +274,13 @@ func InstallAPIs(
|
||||
builders []APIGroupBuilder,
|
||||
storageOpts *options.StorageOptions,
|
||||
reg prometheus.Registerer,
|
||||
namespaceMapper request.NamespaceMapper,
|
||||
kvStore grafanarest.NamespacedKVStore,
|
||||
serverLock ServerLockService,
|
||||
dualWriteService dualwrite.Service,
|
||||
optsregister apistore.StorageOptionsRegister,
|
||||
features featuremgmt.FeatureToggles,
|
||||
dualWriterMetrics *grafanarest.DualWriterMetrics,
|
||||
builderMetrics *BuilderMetrics,
|
||||
apiResourceConfig *serverstorage.ResourceConfig,
|
||||
) error {
|
||||
@@ -282,20 +303,79 @@ func InstallAPIs(
|
||||
// when missing this will default to mode zero (legacy only)
|
||||
var mode = grafanarest.DualWriterMode(0)
|
||||
|
||||
var (
|
||||
err error
|
||||
dualWriterPeriodicDataSyncJobEnabled bool
|
||||
dualWriterMigrationDataSyncDisabled bool
|
||||
dataSyncerInterval = time.Hour
|
||||
dataSyncerRecordsLimit = 1000
|
||||
)
|
||||
|
||||
resourceConfig, resourceExists := storageOpts.UnifiedStorageConfig[key]
|
||||
if resourceExists {
|
||||
mode = resourceConfig.DualWriterMode
|
||||
dualWriterPeriodicDataSyncJobEnabled = resourceConfig.DualWriterPeriodicDataSyncJobEnabled
|
||||
dualWriterMigrationDataSyncDisabled = resourceConfig.DualWriterMigrationDataSyncDisabled
|
||||
dataSyncerInterval = resourceConfig.DataSyncerInterval
|
||||
dataSyncerRecordsLimit = resourceConfig.DataSyncerRecordsLimit
|
||||
}
|
||||
|
||||
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode)
|
||||
// Force using storage only -- regardless of internal synchronization state
|
||||
if mode == grafanarest.Mode5 {
|
||||
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode, grafanarest.Mode5)
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
switch mode {
|
||||
currentMode := mode
|
||||
if !dualWriterMigrationDataSyncDisabled || dualWriterPeriodicDataSyncJobEnabled {
|
||||
// TODO: inherited context from main Grafana process
|
||||
ctx := context.Background()
|
||||
|
||||
// Moving from one version to the next can only happen after the previous step has
|
||||
// successfully synchronized.
|
||||
requestInfo := getRequestInfo(gr, namespaceMapper)
|
||||
|
||||
syncerCfg := &grafanarest.SyncerConfig{
|
||||
Kind: key,
|
||||
RequestInfo: requestInfo,
|
||||
Mode: mode,
|
||||
SkipDataSync: dualWriterMigrationDataSyncDisabled,
|
||||
LegacyStorage: legacy,
|
||||
Storage: storage,
|
||||
ServerLockService: serverLock,
|
||||
DataSyncerInterval: dataSyncerInterval,
|
||||
DataSyncerRecordsLimit: dataSyncerRecordsLimit,
|
||||
}
|
||||
|
||||
// This also sets the currentMode on the syncer config.
|
||||
currentMode, err = grafanarest.SetDualWritingMode(ctx, kvStore, syncerCfg, dualWriterMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// when unable to use
|
||||
if currentMode != mode {
|
||||
klog.Warningf("Requested DualWrite mode: %d, but using %d for %+v", mode, currentMode, gr)
|
||||
}
|
||||
|
||||
if dualWriterPeriodicDataSyncJobEnabled && (currentMode >= grafanarest.Mode1 && currentMode <= grafanarest.Mode3) {
|
||||
// The mode might have changed in SetDualWritingMode, so apply current mode first.
|
||||
syncerCfg.Mode = currentMode
|
||||
if err := grafanarest.StartPeriodicDataSyncer(ctx, syncerCfg, dualWriterMetrics); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode, currentMode)
|
||||
|
||||
switch currentMode {
|
||||
case grafanarest.Mode0:
|
||||
return legacy, nil
|
||||
case grafanarest.Mode4, grafanarest.Mode5:
|
||||
return storage, nil
|
||||
default:
|
||||
return dualwrite.NewStaticStorage(gr, mode, legacy, storage)
|
||||
return dualwrite.NewStaticStorage(gr, currentMode, legacy, storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ func ProvideBuilderMetrics(reg prometheus.Registerer) *BuilderMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BuilderMetrics) RecordDualWriterModes(resource, group string, mode grafanarest.DualWriterMode) {
|
||||
m.dualWriterTargetMode.WithLabelValues(resource, group).Set(float64(mode))
|
||||
m.dualWriterCurrentMode.WithLabelValues(resource, group).Set(float64(mode))
|
||||
func (m *BuilderMetrics) RecordDualWriterModes(resource, group string, targetMode, currentMode grafanarest.DualWriterMode) {
|
||||
m.dualWriterTargetMode.WithLabelValues(resource, group).Set(float64(targetMode))
|
||||
m.dualWriterCurrentMode.WithLabelValues(resource, group).Set(float64(currentMode))
|
||||
}
|
||||
|
||||
func ProvideDualWriterMetrics(reg prometheus.Registerer) *grafanarest.DualWriterMetrics {
|
||||
return grafanarest.NewDualWriterMetrics(reg)
|
||||
}
|
||||
|
||||
@@ -11,16 +11,11 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
|
||||
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
secret "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
@@ -135,7 +130,8 @@ func (v *unifiedStorageConfigValue) Set(val string) error {
|
||||
}
|
||||
|
||||
(*v.config)[key] = setting.UnifiedStorageConfig{
|
||||
DualWriterMode: apiserverrest.DualWriterMode(mode),
|
||||
DualWriterMode: apiserverrest.DualWriterMode(mode),
|
||||
DualWriterMigrationDataSyncDisabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,16 +232,19 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
|
||||
if o.StorageType != StorageTypeUnifiedGrpc {
|
||||
return nil
|
||||
}
|
||||
|
||||
grpcOpts := o.buildGrpcDialOptions()
|
||||
|
||||
conn, err := grpc.NewClient(o.Address, grpcOpts...)
|
||||
conn, err := grpc.NewClient(o.Address,
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var indexConn *grpc.ClientConn
|
||||
if o.SearchServerAddress != "" {
|
||||
indexConn, err = grpc.NewClient(o.SearchServerAddress, grpcOpts...)
|
||||
indexConn, err = grpc.NewClient(o.SearchServerAddress,
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -294,42 +293,3 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
|
||||
serverConfig.RESTOptionsGetter = getter
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildGrpcDialOptions creates gRPC dial options with resilience mechanisms:
|
||||
// - Round-robin load balancing with client-side health checking
|
||||
// - Retry interceptor for transient connection issues
|
||||
// - Keepalive for long-lived connections
|
||||
func (o *StorageOptions) buildGrpcDialOptions() []grpc.DialOption {
|
||||
// Retry interceptor for transient connection issues (codes.Unavailable includes connection refused)
|
||||
retryInterceptor := grpc_retry.UnaryClientInterceptor(
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.5)),
|
||||
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
|
||||
)
|
||||
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithChainUnaryInterceptor(retryInterceptor),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
}),
|
||||
}
|
||||
|
||||
if o.GrpcClientKeepaliveTime > 0 {
|
||||
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: o.GrpcClientKeepaliveTime,
|
||||
Timeout: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}))
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -30,8 +30,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apiserver/auditing"
|
||||
grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/modules"
|
||||
@@ -44,6 +47,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authenticator"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
@@ -93,8 +97,10 @@ type service struct {
|
||||
tracing *tracing.TracingService
|
||||
metrics prometheus.Registerer
|
||||
|
||||
authorizer *authorizer.GrafanaAuthorizer
|
||||
dualWriter dualwrite.Service
|
||||
authorizer *authorizer.GrafanaAuthorizer
|
||||
serverLockService builder.ServerLockService
|
||||
dualWriter dualwrite.Service
|
||||
kvStore kvstore.KVStore
|
||||
|
||||
pluginClient plugins.Client
|
||||
datasources datasource.ScopedPluginDatasourceProvider
|
||||
@@ -108,6 +114,7 @@ type service struct {
|
||||
aggregatorRunner aggregatorrunner.AggregatorRunner
|
||||
appInstallers []appsdkapiserver.AppInstaller
|
||||
builderMetrics *builder.BuilderMetrics
|
||||
dualWriterMetrics *grafanarest.DualWriterMetrics
|
||||
|
||||
auditBackend audit.Backend
|
||||
auditPolicyRuleProvider auditing.PolicyRuleProvider
|
||||
@@ -118,7 +125,9 @@ func ProvideService(
|
||||
features featuremgmt.FeatureToggles,
|
||||
rr routing.RouteRegister,
|
||||
tracing *tracing.TracingService,
|
||||
serverLockService *serverlock.ServerLockService,
|
||||
db db.DB,
|
||||
kvStore kvstore.KVStore,
|
||||
pluginClient plugins.Client,
|
||||
datasources datasource.ScopedPluginDatasourceProvider,
|
||||
contextProvider datasource.PluginContextWrapper,
|
||||
@@ -150,10 +159,12 @@ func ProvideService(
|
||||
tracing: tracing,
|
||||
db: db, // For Unified storage
|
||||
metrics: reg,
|
||||
kvStore: kvStore,
|
||||
pluginClient: pluginClient,
|
||||
datasources: datasources,
|
||||
contextProvider: contextProvider,
|
||||
pluginStore: pluginStore,
|
||||
serverLockService: serverLockService,
|
||||
dualWriter: dualWriter,
|
||||
unified: unified,
|
||||
secrets: secrets,
|
||||
@@ -162,6 +173,7 @@ func ProvideService(
|
||||
aggregatorRunner: aggregatorRunner,
|
||||
appInstallers: appInstallers,
|
||||
builderMetrics: builderMetrics,
|
||||
dualWriterMetrics: grafanarest.NewDualWriterMetrics(reg),
|
||||
auditBackend: auditBackend,
|
||||
auditPolicyRuleProvider: auditPolicyRuleProvider,
|
||||
}
|
||||
@@ -400,9 +412,13 @@ func (s *service) start(ctx context.Context) error {
|
||||
builders,
|
||||
o.StorageOptions,
|
||||
s.metrics,
|
||||
request.GetNamespaceMapper(s.cfg),
|
||||
kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"),
|
||||
s.serverLockService,
|
||||
s.dualWriter,
|
||||
optsregister,
|
||||
s.features,
|
||||
s.dualWriterMetrics,
|
||||
s.builderMetrics,
|
||||
apiResourceConfig,
|
||||
)
|
||||
@@ -416,7 +432,11 @@ func (s *service) start(ctx context.Context) error {
|
||||
server,
|
||||
serverConfig.RESTOptionsGetter,
|
||||
o.StorageOptions,
|
||||
kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"),
|
||||
s.serverLockService,
|
||||
request.GetNamespaceMapper(s.cfg),
|
||||
s.dualWriter,
|
||||
s.dualWriterMetrics,
|
||||
s.builderMetrics,
|
||||
serverConfig.MergedResourceConfig,
|
||||
); err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
builder.ProvideDualWriterMetrics,
|
||||
builder.ProvideBuilderMetrics,
|
||||
ProvideEventualRestConfigProvider,
|
||||
wire.Bind(new(RestConfigProvider), new(*eventualRestConfigProvider)),
|
||||
|
||||
@@ -1073,6 +1073,13 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
},
|
||||
{
|
||||
Name: "unifiedStorageSearchSprinkles",
|
||||
Description: "Enable sprinkles on unified storage search",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "managedDualWriter",
|
||||
Description: "Pick the dual write mode from database configs",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user